Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Created September 25, 2019 12:36
Show Gist options
  • Save swiftui-lab/a2fec9c4eff874e8ae21076763744335 to your computer and use it in GitHub Desktop.
Save swiftui-lab/a2fec9c4eff874e8ae21076763744335 to your computer and use it in GitHub Desktop.
// Advanced SwiftUI Transitions
// https://swiftui-lab.com
// https://swiftui-lab.com/advanced-transitions
import SwiftUI
struct GeometryEffectTransitionsDemo: View {
@State private var show = false
var body: some View {
return ZStack {
Button("Open Booking") {
withAnimation(.easeInOut(duration: 0.8)) {
self.show = true
}
}
if show {
RoundedRectangle(cornerRadius: 15)
.fill(Color.pink).overlay(MyForm(show: $show))
.frame(width: 400, height: 500)
.shadow(color: .black, radius: 3)
.transition(.fly)
.zIndex(1)
}
}
}
}
struct MyForm: View {
@Binding var show: Bool
@State private var departure = Date()
@State private var checkin = Date()
@State private var pets = true
@State private var nonsmoking = true
@State private var airport: Double = 7.3
var body: some View {
VStack {
Text("Booking").font(.title).foregroundColor(.white)
Form {
DatePicker(selection: $departure, label: {
HStack {
Image(systemName: "airplane")
Text("Departure")
}
})
DatePicker(selection: $checkin, label: {
HStack {
Image(systemName: "house.fill")
Text("Check-In")
}
})
Toggle(isOn: $pets, label: { HStack { Image(systemName: "hare.fill"); Text("Have Pets") } })
Toggle(isOn: $nonsmoking, label: { HStack { Image(systemName: "nosign"); Text("Non-Smoking") } })
Text("Max Distance to Airport \(String(format: "%.2f", self.airport as Double)) km")
Slider(value: $airport, in: 0...10) { EmptyView() }
Button(action: {
withAnimation(.easeInOut(duration: 1.0)) {
self.show = false
}
}) {
HStack { Spacer(); Text("Save"); Spacer() }
}
}
}.padding(20)
}
}
extension AnyTransition {
static var fly: AnyTransition { get {
AnyTransition.modifier(active: FlyTransition(pct: 0), identity: FlyTransition(pct: 1))
}
}
}
struct FlyTransition: GeometryEffect {
var pct: Double
var animatableData: Double {
get { pct }
set { pct = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let rotationPercent = pct
let a = CGFloat(Angle(degrees: 90 * (1-rotationPercent)).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, 1, 0, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
let affineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(pct * 2), y: CGFloat(pct * 2)))
if pct <= 0.5 {
return ProjectionTransform(transform3d).concatenating(affineTransform2).concatenating(affineTransform1)
} else {
return ProjectionTransform(transform3d).concatenating(affineTransform1)
}
}
}
@igavrysh
Copy link

igavrysh commented Apr 20, 2025

there is a strange blinking in the start of animation, and message in console

ignoring singular matrix: ProjectionTransform(m11: 0.0, m12: 0.0,
m13: 0.0, m21: -0.33361111111111114, m22: -0.5, m23: -0.0016666666666666668, 
m31: 300.25, m32: 450.0, m33: 1.5)

I solved this by changing active transition from FlyTransition(pct: 0.0) to FlyTransition(pct: 0.001) + some changes in DatePicker size...

updated version:

import SwiftUI

struct GeometryEffectTransitionsDemo: View {
    @State private var show = false

    var body: some View {
        return ZStack {
            Button("Open Booking") {
                withAnimation(.easeInOut(duration: 0.8)) {
                    self.show = true
                }
            }
            if show {
                RoundedRectangle(cornerRadius: 15)
                    .fill(Color.pink).overlay(MyForm(show: $show))
                    .frame(width: 400, height: 600)
                    .shadow(color: .black, radius: 3)
                    .transition(.fly)
                    .zIndex(1)
            }
        }
    }
}

struct MyForm: View {
    @Binding var show: Bool

    @State private var departure = Date()
    @State private var checkin = Date()

    @State private var pets = true
    @State private var nonsmoking = true
    @State private var airport: Double = 7.3

    var body: some View {
        VStack {
            Text("Booking").font(.title).foregroundColor(.white)

            Form {
                VStack(alignment: .leading, spacing: 4) {
                    HStack {
                        Image(systemName: "airplane")
                        Text("Departure")
                    }
                    DatePicker("", selection: $departure).labelsHidden()
                        .frame(maxWidth: .infinity, alignment: .leading)
                }

                VStack(alignment: .leading, spacing: 4) {
                    HStack {
                        Image(systemName: "house.fill")
                        Text("Check-In")
                    }
                    DatePicker("", selection: $checkin).labelsHidden()
                        .frame(maxWidth: .infinity, alignment: .leading)

                }

                Toggle(isOn: $pets) {
                    HStack {
                        Image(systemName: "hare.fill")
                        Text("Have Pets")
                    }
                }

                Toggle(isOn: $nonsmoking) {
                    HStack {
                        Image(systemName: "nosign")
                        Text("Non-Smoking")
                    }
                }

                Text("Max Distance to Airport \(String(format: "%.2f", self.airport as Double)) km")
                Slider(value: $airport, in: 0...10) { EmptyView() }

                Button(action: {
                    withAnimation(.easeInOut(duration: 1.0)) {
                        self.show = false
                    }
                }) {
                    HStack { Spacer(); Text("Save"); Spacer() }
                }
            }
        }
        .padding(20)
    }
}

extension AnyTransition {
    static var fly: AnyTransition {
        get {
            AnyTransition.modifier(active: FlyTransition(pct: 0.0), identity: FlyTransition(pct: 1))
        }
    }
}

struct FlyTransition: GeometryEffect {
    var pct: Double

    var animatableData: Double {
        get { pct }
        set { pct = newValue }
    }

    func effectValue(size: CGSize) -> ProjectionTransform {
        let rotationPercent = pct
        let a = CGFloat(Angle(degrees: 90 * (1 - rotationPercent)).radians)

        var transform3d = CATransform3DIdentity
        transform3d.m34 = -1 / max(size.width, size.height)

        transform3d = CATransform3DRotate(transform3d, a, 1, 0, 0)
        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)

        let affineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height/2.0))
        let affineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(pct * 2), y: CGFloat(pct * 2)))

        if pct <= 0.5 {
            return ProjectionTransform(transform3d).concatenating(affineTransform2).concatenating(affineTransform1)
        } else {
            return ProjectionTransform(transform3d).concatenating(affineTransform1)
        }
    }
}

#Preview {
    GeometryEffectTransitionsDemo()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment