Skip to main content

How to create multi-step animations using phase animators

About 4 minSwiftSwiftUIArticle(s)bloghackingwithswift.comcrashcourseswiftswiftuixcodeappstore

How to create multi-step animations using phase animators 관련

SwiftUI by Example

Back to Home

How to create multi-step animations using phase animators | SwiftUI by Example

How to create multi-step animations using phase animators

Updated for Xcode 15

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 projectopen in new window

The text Hello World zooming up and fading out repeatedly.
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 projectopen in new window

The text Hello World zooming up and fading out repeatedly, while the text Goodbye World zooms out while fading out at the same time.
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 projectopen in new window

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 projectopen in new window

A button that says Tap Me, which zooms up, becomes blurry, then resets every time it’s pressed.
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 projectopen in new window

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
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 projectopen in new window

The text Hello World zooming up and fading out repeatedly.
The text Hello World zooming up and fading out repeatedly.
Similar solutions…
How to override animations with transactions | SwiftUI by Example

How to override animations with transactions
How to apply multiple animations to a view | SwiftUI by Example

How to apply multiple animations to a view
How to create basic animations | SwiftUI by Example

How to create basic animations
How to synchronize animations from one view to another with matchedGeometryEffect() | SwiftUI by Example

How to synchronize animations from one view to another with matchedGeometryEffect()
How to reduce animations when requested | SwiftUI by Example

How to reduce animations when requested

이찬희 (MarkiiimarK)
Never Stop Learning.