WWDC Quick Look 💓 By SwiftGGTeam
Wind your way through advanced animations in SwiftUI

Wind your way through advanced animations in SwiftUI

Watch original video

Highlight

SwiftUI introduces two new APIs in iOS 17:phaseAnimatorFor defining multi-stage loop/event-driven animations,keyframeAnimatorUsed to create multi-track keyframe animations, both support customizing animation curves at each stage, allowing the implementation of complex animations to change from “handwritten state machine” to “declarative configuration”.

Core Content

Limitations of traditional animation

Tim, from the SwiftUI team, points outwithAnimationOnly transitions “from old state to new state” can be handled. Many animation requirements are more complex: loading indicators need to loop continuously, and buttons need to play a pulse effect and then resume after clicking. These animations have multiple steps, and the steps are not simple state switches.

Phase Animator: Multi-stage animation

03:13phaseAnimatorThe modifier accepts a set of phases, and SwiftUI automatically cycles between them.

The simplest example is a two-phase loop - highlight and normal:

OverdueReminderView()
    .phaseAnimator([false, true]) { content, value in
        content
            .foregroundStyle(value ? .red : .primary)
    } animation: { _ in
        .easeInOut(duration: 1.0)
    }

Key points:

  • Provide stage array, SwiftUI automatically loops
  • Any attribute of the view can be modified at each stage -animationThe closure receives the target stage and can specify different animations for different stages.
  • Spring is used by default and can be usedanimationClosure coverage

(06:20) Define more complex stages with custom enums:

ReactionView()
    .phaseAnimator(
        Phase.allCases,
        trigger: reactionCount
    ) { content, phase in
        content
            .scaleEffect(phase.scale)
            .offset(y: phase.verticalOffset)
    } animation: { phase in
        switch phase {
        case .initial: .smooth
        case .move: .easeInOut(duration: 0.3)
        case .scale: .spring(duration: 0.3, bounce: 0.7)
        }
    }

enum Phase: CaseIterable {
    case initial
    case move
    case scale

    var verticalOffset: Double {
        switch self {
        case .initial: 0
        case .move, .scale: -64
        }
    }

    var scale: Double {
        switch self {
        case .initial: 1.0
        case .move: 1.1
        case .scale: 1.8
        }
    }
}

Key points:

  • Custom enums followCaseIterable, each case defines the attribute value of this stage -triggerParameters make the animation play once when the event is triggered (rather than looping)
  • Each stage can have its own animation curve

Keyframe Animator: keyframe animation

09:48keyframeAnimatorAllows defining separate tracks for each animation property, each with its own set of keyframes.

struct AnimationValues {
    var scale = 1.0
    var verticalStretch = 1.0
    var verticalTranslation = 0.0
    var angle = Angle.zero
}

ReactionView()
    .keyframeAnimator(initialValue: AnimationValues()) { content, value in
        content
            .foregroundStyle(.red)
            .rotationEffect(value.angle)
            .scaleEffect(value.scale)
            .scaleEffect(y: value.verticalStretch)
            .offset(y: value.verticalTranslation)
    } keyframes: { _ in
        KeyframeTrack(\.angle) {
            CubicKeyframe(.zero, duration: 0.58)
            CubicKeyframe(.degrees(16), duration: 0.125)
            CubicKeyframe(.degrees(-16), duration: 0.125)
            CubicKeyframe(.degrees(16), duration: 0.125)
            CubicKeyframe(.zero, duration: 0.125)
        }

        KeyframeTrack(\.verticalStretch) {
            CubicKeyframe(1.0, duration: 0.1)
            CubicKeyframe(0.6, duration: 0.15)
            CubicKeyframe(1.5, duration: 0.1)
            CubicKeyframe(1.05, duration: 0.15)
            CubicKeyframe(1.0, duration: 0.88)
            CubicKeyframe(0.8, duration: 0.1)
            CubicKeyframe(1.04, duration: 0.4)
            CubicKeyframe(1.0, duration: 0.22)
        }

        KeyframeTrack(\.scale) {
            LinearKeyframe(1.0, duration: 0.36)
            SpringKeyframe(1.5, duration: 0.8, spring: .bouncy)
            SpringKeyframe(1.0, spring: .bouncy)
        }

        KeyframeTrack(\.verticalTranslation) {
            LinearKeyframe(0.0, duration: 0.1)
            SpringKeyframe(20.0, duration: 0.15, spring: .bouncy)
            SpringKeyframe(-60.0, duration: 1.0, spring: .bouncy)
            SpringKeyframe(0.0, spring: .bouncy)
        }
    }

Key points:

  • initialValueDefine the initial value when the animation starts
  • eachKeyframeTrackAn animation track corresponding to an attribute
  • Keyframe type:LinearKeyframe(linear interpolation),CubicKeyframe(cubic curve),SpringKeyframe(spring animation)
  • Different tracks can have different durations and number of keyframes
  • Tracks can be interlaced to produce rich combination effects

Map keyframe animation

(15:22) Keyframe animation can also be usedMapcamera:

Map(initialPosition: .rect(route.rect)) {
    MapPolyline(coordinates: route.coordinates)
        .stroke(.orange, lineWidth: 4.0)
    Marker("Start", coordinate: route.start)
        .tint(.green)
    Marker("End", coordinate: route.end)
        .tint(.red)
}
.mapCameraKeyframeAnimation(trigger: playTrigger) { initialCamera in
    KeyframeTrack(\.centerCoordinate) {
        let points = route.points
        for point in points {
            CubicKeyframe(point.coordinate, duration: 16.0 / Double(points.count))
        }
        CubicKeyframe(initialCamera.centerCoordinate, duration: 4.0)
    }
    KeyframeTrack(\.heading) {
        CubicKeyframe(heading(from: route.start.coordinate, to: route.end.coordinate), duration: 6.0)
        CubicKeyframe(heading(from: route.end.coordinate, to: route.end.coordinate), duration: 8.0)
        CubicKeyframe(initialCamera.heading, duration: 6.0)
    }
    KeyframeTrack(\.distance) {
        CubicKeyframe(24000, duration: 4)
        CubicKeyframe(18000, duration: 12)
        CubicKeyframe(initialCamera.distance, duration: 4)
    }
}

Key points:

  • mapCameraKeyframeAnimationIs a Map-specific keyframe animation modifier
  • Can control the center coordinates, heading and distance of the camera
  • The camera flies smoothly along the route and finally returns to the initial position

KeyframeTimeline

16:26KeyframeTimelineCan be used independently without relying on views:

let myKeyframes = KeyframeTimeline(initialValue: CGPoint.zero) {
    KeyframeTrack(\.x) { ... }
    KeyframeTrack(\.y) { ... }
}

let duration: TimeInterval = myKeyframes.duration
let value = myKeyframes.value(time: 1.2)

Key points:

  • You can query the total duration of the animation
  • You can query the value at any point in time
  • Suitable for custom animation logic or integration with other systems

Detailed Content

How Phase Animator works

(03:57) Phase Animator workflow:

  1. The first phase is activated when the view first appears
  2. SwiftUI immediately starts the animation transition to the next stage
  3. After the animation is completed, it automatically advances to the next stage.
  4. After reaching the last stage, loop back to the first stage

if providedtriggerParameter, the animation plays all stages once when the trigger value changes, and then stops.

Detailed explanation of keyframe types

Each of the three keyframe types has its applicable scenarios:

  • LinearKeyframe: changes at a constant speed, suitable for attributes that require constant speed
  • CubicKeyframe: smooth curve, suitable for most visual properties
  • SpringKeyframe: Physical spring effect, suitable for properties that require elasticity

SpringKeyframeYou can just specify the target value and spring parameters and let SwiftUI automatically calculate the duration.

Core Takeaways

  1. Loop reminder animation

    • What to do: Add breathing light effects to to-do items and notification reminders
    • Why it’s worth doing:phaseAnimatorMake looping animations a few lines of code, no timers or state machines required
    • How to start: Define an array of two stages (normal/highlight), with.phaseAnimatorModify the reminder view, inanimationspecified in the closure.easeInOut(duration: 1.5)
  2. Like/Collect animation

    • What it does: Play a multi-stage heart-shaped zoom animation when a user likes it
    • Why it’s worth doing:triggerPatterns make event-driven animation simple, allowing you to combine scaling, rotation, and color changes
    • How to start: Definitioninitial -> scaleUp -> settlethree stages, usingtrigger: likeCountTrigger, each stage specifies different spring parameters
  3. Map Route Roaming

    • What to do: Display a running/cycling route on the map, with the camera flying along the route
    • Why it’s worth doing:mapCameraKeyframeAnimationTurn complex camera controls into declarative configuration
    • How to start: Prepare the coordinate point array of the route, ascenterCoordinateTrack creation keyframes, one for each pointCubicKeyframe
  4. Loading Status Indicator

    • What to do: Customize loading animation, than systemProgressViewMore brand characteristics
    • Why it’s worth doing:KeyframeTimelineCan be used independently or in conjunction withCanvasDraw animations of arbitrary shapes
    • How to start: CreateKeyframeTimelineTo define changes in shape properties over time, useTimelineViewDriver redraw
  5. Complex transition animation

    • What it does: Play a multi-property combination animation when the view appears/disappears
    • Why it’s worth doing: Keyframe animation allows multiple attributes (position, scaling, rotation, transparency) to be controlled independently and coordinated
    • How to start: Define a structure containing all animation properties, create aKeyframeTrack, applied in view modifiers.keyframeAnimator

Comments

GitHub Issues · utterances