Advanced Animation Techniques

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.

The recipe for using it is simple:

  1. Define your steps, usually as an enum. This makes your code very readable.

  2. Create the PhaseAnimator view, giving it your sequence of phases and a trigger to start it.

  3. Write your view code inside the animator, making its appearance change based on the current phase.

  4. Optionally, provide an animation block to customize the transition between phases.

This example builds a progress bar that animates to full in stages every time you tap it. The enum for the phases includes a computed property (widthMultiplier), which makes the code inside the animator incredibly clean and readable.

Here is the view code for the PhaseAnimator example.

// 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.

This gives you precise, granular control for creating custom bounces, jiggles, and other complex effects that a standard animation curve can’t achieve. This example creates a complex, multi-part animation on a piece of text.

Here is the view code for the KeyframeAnimator example.

// 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()
        }
    }
}

SwiftUI provides four types of keyframes for the KeyframeAnimator: Linear, Cubic, Spring, and Move.

LinearKeyframe

A LinearKeyframe creates a direct, constant-speed transition between the previous value and the new target value. Think of it as drawing a straight line between two points. The animation proceeds at a steady rate without any acceleration or deceleration.

Use Case: Best for uniform motion, like a loading bar filling up or an object moving at a constant velocity.

// Moves from the previous scale to 1.5 at a constant speed over 0.2 seconds
LinearKeyframe(1.5, duration: 0.2)

CubicKeyframe

A CubicKeyframe animates a value along a cubic Bézier curve. This allows for smooth acceleration and deceleration, often called “easing.” It gives animations a more natural and polished feel compared to the mechanical movement of a linear keyframe. This is the most common type of timing curve used in UI animation.

Use Case: Ideal for most UI transitions, like an element sliding into view (“ease-out”), a modal disappearing (“ease-in”), or a button changing state (“ease-in-out”).

// Smoothly eases from the previous offset to 20 over 0.2 seconds
CubicKeyframe(20, duration: 0.2)

SpringKeyframe

A SpringKeyframe animates a value using spring physics to create a bouncy, dynamic, and realistic effect. Instead of just moving to a target, the value will overshoot it and oscillate back and forth before settling. You can customize the spring’s behavior by providing parameters like stiffness and damping.

Use Case: Perfect for animations that should feel physical and playful, like a “like” button bouncing when tapped, a notification wiggling for attention, or an object reacting to being dropped.

// "Bounces" towards a rotation of -30 degrees
SpringKeyframe(Angle.degrees(-30), duration: 0.2)

MoveKeyFrame

The MoveKeyframe is a keyframe that immediately moves to the given value without interpolation. Unlike LinearKeyframe, CubicKeyframe, and SpringKeyframe which smoothly transition over time, MoveKeyframe simply jumps to the target value.

Use Case: Use when you need to switch between distinct states without any visual transition.

// image immediately appears
MoveKeyframe("image")
// immediately switch to image2
MoveKeyframe(at: 1.0, "image2")

Let’s put all the animations into a runnable app.

See forum comments
Download course materials from Github
Previous: Creating Your First Animation Next: Putting it all Together