Advanced Animation Techniques

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

While withAnimation is the powerful workhorse for most animations, SwiftUI also provides specialized tools for more complex, narrative-style animations where you need greater control over the sequence and timing.

PhaseAnimator

PhaseAnimator is the modern, declarative replacement for older, more manual techniques like using a Timer to cycle through states. It is designed for animations that happen in a clear sequence of distinct steps.

// MARK: - Simpler PhaseAnimator Example
struct ProgressAnimatorView: View {
    // 1. Define the phases with a clear name and a computed property
    enum ProgressPhase: CaseIterable {
        case initial, quarter, half, threeQuarters, full
        
        var widthMultiplier: Double {
            switch self {
            case .initial: 0.0
            case .quarter: 0.25
            case .half: 0.5
            case .threeQuarters: 0.75
            case .full: 1.0
            }
        }
    }
    
    // 2. Use a simple integer trigger to replay the animation on each tap.
    @State private var progressTrigger = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("PhaseAnimator")
                .font(.title)
                .fontWeight(.bold)
            
            // 3. The animator runs its sequence every time the trigger value changes.
            PhaseAnimator(ProgressPhase.allCases, trigger: progressTrigger) { phase in
                GeometryReader { geo in
                    ZStack(alignment: .leading) {
                        Capsule().fill(.gray.opacity(0.3))
                        Capsule()
                            .fill(.green)
                            .frame(width: geo.size.width * phase.widthMultiplier)
                    }
                }
            } animation: { _ in
                .spring(duration: 0.6, bounce: 0.4)
            }
            .frame(height: 20)
            
            Text("Tap the progress bar to animate it.")
                .font(.caption)
                .foregroundColor(.secondary)
            
            Spacer()
        }
        .onTapGesture {
            progressTrigger += 1
        }
    }
}

KeyframeAnimator

While PhaseAnimator is for a sequence of states, KeyframeAnimator is for controlling the value of a property over time within a single animation. If PhaseAnimator is like a flipbook with a few distinct pages, KeyframeAnimator is like being a puppeteer, precisely controlling the puppet’s strings at specific moments to create a single, seamless motion.

// MARK: - KeyframeAnimator Example
struct KeyframeAnimatorView: View {
    struct AnimationValues {
        var scale: Double = 1.0
        var verticalOffset: Double = 0.0
        var opacity: Double = 1.0
    }
    
    @State private var animationTrigger = 0

    var body: some View {
        VStack {
            Text("KeyframeAnimator")
                .font(.title)
                .fontWeight(.bold)
            
            Text("Hello, Keyframes!")
                .keyframeAnimator(
                    initialValue: AnimationValues(),
                    trigger: animationTrigger
                ) { content, value in
                    content
                        .scaleEffect(value.scale)
                        .offset(y: value.verticalOffset)
                        .opacity(value.opacity)
                } keyframes: { _ in
                    KeyframeTrack(\.scale) {
                        SpringKeyframe(1.2, duration: 0.3, spring: .bouncy)
                        SpringKeyframe(1.0, spring: .bouncy)
                    }
                    KeyframeTrack(\.verticalOffset) {
                        LinearKeyframe(-20, duration: 0.2)
                        LinearKeyframe(0, duration: 0.5)
                    }
                    KeyframeTrack(\.opacity) {
                        LinearKeyframe(1.0, duration: 0.6)
                        LinearKeyframe(0.0, duration: 0.2)
                    }
                }
                .onTapGesture {
                    animationTrigger += 1
                }
            
            Text("Tap the text!")
                .font(.caption)
                .foregroundColor(.secondary)
                
            Spacer()
        }
    }
}
// Moves from the previous scale to 1.5 at a constant speed over 0.2 seconds
LinearKeyframe(1.5, duration: 0.2)
// Smoothly eases from the previous offset to 20 over 0.2 seconds
CubicKeyframe(20, duration: 0.2)
// "Bounces" towards a rotation of -30 degrees
SpringKeyframe(Angle.degrees(-30), duration: 0.2)
// image immediately appears
MoveKeyframe("image")
// immediately switch to image2
MoveKeyframe(at: 1.0, "image2")
See forum comments
Download course materials from Github
Previous: Creating Your First Animation Next: Putting it all Together