BLACK FRIDAY: Save 50% on all my books and bundles! >>

How to create multi-step animations using phase animators

Paul Hudson    @twostraws   

Updated for Xcode 16.4

New in iOS 17

SwiftUI’s PhaseAnimator view and phaseAnimator modifier allow us to perform multi-step animation by cycling through animation phases of our choosing, either constantly or when triggered.

Creating these multi-phase animations takes three steps:

  1. Define the phases you’re going to work through. This can be any kind of sequence, but you might find it easiest to work with a CaseIterable enum.
  2. Read one phase inside your phase animator, and adjust your views to match how you want that phase to look.
  3. Optionally add a trigger to make the phase animator repeat its sequence from the beginning. Without this it will cycle constantly.

For example, this next example creates a simple animation that makes some text start small and invisible, scale up to natural size and be fully opaque, then scale up to be very large and invisible. It uses an array of the numbers 0, 1, and 3 to represent the various scaling sizes we’ll be using (0%, 100%, and 300%), and it makes the text opaque when the size is 1:

Text("Hello, world!")
    .font(.largeTitle)
    .phaseAnimator([0, 1, 3]) { view, phase in
        view
            .scaleEffect(phase)
            .opacity(phase == 1 ? 1 : 0)
    }

Download this as an Xcode project

The text Hello World zooming up and fading out repeatedly.

Because we haven’t provided a trigger for the animation, it will run forever.

If you prefer, you can write that using a wrapping PhaseAnimator view, which has the advantage that multiple views can move between phases together:

VStack(spacing: 50) {
    PhaseAnimator([0, 1, 3]) { value in
        Text("Hello, world!")
            .font(.largeTitle)
            .scaleEffect(value)
            .opacity(value == 1 ? 1 : 0)

        Text("Goodbye, world!")
            .font(.largeTitle)
            .scaleEffect(3 - value)
            .opacity(value == 1 ? 1 : 0)
    }
}

Download this as an Xcode project

The text Hello World zooming up and fading out repeatedly, while the text Goodbye World zooms out while fading out at the same time.

Like I said, you might prefer to use an enum with your various phases. This might have meaningful raw values attached, but it doesn’t need to. Here’s the same thing rewritten using an enum:

enum AnimationPhase: Double, CaseIterable {
    case fadingIn = 0
    case middle = 1
    case zoomingOut = 3
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .font(.largeTitle)
            .phaseAnimator(AnimationPhase.allCases) { view, phase in
                view
                    .scaleEffect(phase.rawValue)
                    .opacity(phase.rawValue == 1 ? 1 : 0)
            }
    }
}

Download this as an Xcode project

Rather than have the phase animators repeat endlessly, you can make it trigger the animation sequence on your command. To do this, attach a trigger value for SwiftUI to watch, such as a random UUID or an incrementing number. Whenever that value changes, SwiftUI will reset your animator and play it back in full.

In this next example, tapping the button triggers a three-step animation using enum cases. First, we define the various animation phases we want, then we move through them whenever a property changes:

enum AnimationPhase: CaseIterable {
    case start, middle, end
}

struct ContentView: View {
    @State private var animationStep = 0

    var body: some View {
        Button("Tap Me!") {
            animationStep += 1
        }
        .font(.largeTitle)
        .phaseAnimator(AnimationPhase.allCases, trigger: animationStep) { content, phase in
            content
                .blur(radius: phase == .start ? 0 : 10)
                .scaleEffect(phase == .middle ? 3 : 1)
        }
    }
}

Download this as an Xcode project

A button that says Tap Me, which zooms up, becomes blurry, then resets every time it’s pressed.

For even more control, you can specify exactly which animation to use for each phase. For example, this moves between quick .bouncy and a slow .easeInOut animations to get a more varied movement:

enum AnimationPhase: CaseIterable {
    case start, middle, end
}

struct ContentView: View {
    @State private var animationStep = 0

    var body: some View {
        Button("Tap Me!") {
            animationStep += 1
        }
        .font(.largeTitle)
        .phaseAnimator(AnimationPhase.allCases, trigger: animationStep) { content, phase in
            content
                .blur(radius: phase == .start ? 0 : 10)
                .scaleEffect(phase == .middle ? 3 : 1)
        } animation: { phase in
            switch phase {
            case .start, .end: .bouncy
            case .middle: .easeInOut(duration: 2)
            }
        }
    }
}

Download this as an Xcode project

A button that says Tap Me, which zooms up, becomes blurry, then resets every time it’s pressed. The zoom up part of the animation runs slowly.

One approach I’ve found useful is to add extra computed properties to the animation phases to make the rest of the code easier to read, like this:

enum AnimationPhase: CaseIterable {
    case fadingIn, middle, zoomingOut

    var scale: Double {
        switch self {
        case .fadingIn: 0
        case .middle: 1
        case .zoomingOut: 3
        }
    }

    var opacity: Double {
        switch self {
        case .fadingIn: 0
        case .middle: 1
        case .zoomingOut: 0
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .font(.largeTitle)
            .phaseAnimator(AnimationPhase.allCases) { content, phase in
                content
                    .scaleEffect(phase.scale)
                    .opacity(phase.opacity)
            }
    }
}

Download this as an Xcode project

The text Hello World zooming up and fading out repeatedly.

Save 50% in my Black Friday sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further for less! Get my all-new book Everything but the Code to make more money with apps, get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn Swift Testing, design patterns, and more.

Save 50% on all our books and bundles!

Similar solutions…

BUY OUR BOOKS
Buy Everything but the Code Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Interview Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.0/5

 
Unknown user

You are not logged in

Log in or create account