SwiftUI Field Guide

Transitions

In SwiftUI the view structs we write turn into nodes in the attribute graph. SwiftUI maintains identity for the nodes it creates. Whenever a node is inserted, removed or the identity changes, this change can then be animated through a transition.

A transition has three phases: after the view is inserted, or before it is removed, the view (or rather: the transition) is in its identity phase. While the view is appearing it's in the willAppear state and while it's disappearing it's in the didDisappear phase.

Built-in Transitions

Here's an example that shows most of the builtin transitions. You can pick a transition using the dropdown. Note that not all transitions are symmetric: the push(from: .leading) transition will animate the view in from the leading edge, but animate it out through the trailing edge:

Code
struct TransitionExample: View {    @State() private var isVisible = true        var body: some View {        VStack {            if isVisible {                Color.blue                    .frame(width: 96, height: 64)                    .transition(.opacity)            }        }        .frame(maxWidth: .infinity)        .contentShape(.rect)        .onTapGesture {            withAnimation(.easeInOut(                              duration: 0.6                          )) {                isVisible.toggle()            }        }        .padding(20)    }}
Preview

Combining Transitions

You can also combine these transitions yourself. A raw scale transition might look a bit harsh, and is often combined with an opacity.

Code
struct TransitionExample: View {    @State() private var isVisible = true        var body: some View {        VStack {            if isVisible {                Color.blue                    .frame(width: 96, height: 64)                    .transition(.opacity.combined(                                    with: .scale                                ))            }        }        .frame(maxWidth: .infinity)        .contentShape(.rect)        .onTapGesture {            withAnimation(.easeInOut(                              duration: 0.6                          )) {                isVisible.toggle()            }        }        .padding(20)    }}
Preview

A useful mental model is: a transition is a view modifier that is applied while the view is transitioning. Combining transitions ultimately boils down to combining view modifiers, and the order might matter. For example, a .scale.combined(with: .push(from: .leading)) is not the same as .push(from: .leading).combined(with: .scale):

Code
let scaleThenPush = AnyTransition.scale    .combined(with: .push(from: .leading))let pushThenScale = AnyTransition.push(    from: .leading)    .combined(with: .scale)
Preview
Scale, then push
Loading MiniSwiftUI…
Push, then scale
Loading MiniSwiftUI…
Phase
0.4

Asymmetric Transitions

You can also combine transitions in a different way: using the .asymmetric static function on AnyTransition you can have one transition for insertion and a different one for removal. This is how you could construct the push transition yourself:

Code
struct TransitionExample: View {    @State() private var isVisible = true        private var transition: AnyTransition {        .asymmetric(            insertion: .move(                edge: .leading            ),            removal: .opacity        )    }        var body: some View {        VStack {            if isVisible {                Color.blue                    .frame(width: 96, height: 64)                    .transition(transition)            }        }        .frame(maxWidth: .infinity)        .contentShape(.rect)        .onTapGesture {            withAnimation(.easeInOut(                              duration: 0.6                          )) {                isVisible.toggle()            }        }        .padding(20)    }}
Preview

A Custom Blur Transition

You can built completely custom transitions by conforming to the Transition protocol. For example, you could build a blur transition:

Here's what that looks like:

Code
struct BlurTransition: Transition {    func body(content: Content,              phase: TransitionPhase) -> some View {        content            .blur(                radius: phase.isIdentity ? 0 : 10            )            .opacity(phase.isIdentity ? 1 : 0)    }}struct TransitionExample: View {    @State() private var isVisible = true        var body: some View {        VStack {            if isVisible {                Color.blue                    .frame(width: 96, height: 64)                    .transition(BlurTransition())            }        }        .frame(maxWidth: .infinity)        .contentShape(.rect)        .onTapGesture {            withAnimation(.easeInOut(                              duration: 0.6                          )) {                isVisible.toggle()            }        }        .padding(20)    }}
Preview

A Digit Counter

For some animations, a transition can be simpler than building the animation through other means. For example, if we wanted to build a digit counter ourselves we can give the digits identity based on their value. This means that if a digit changes, the identity changes and the old digit is transitioned out while the new digit is transitioned in.

First, we can build the digit transition declaration. This animates up when the value will be increasing and down when it will be decreasing. It's built using AnyTransition but we could have also created a custom transition struct that conforms to the Transition protocol.

Code
private extension AnyTransition {    static func digit(up: Bool) -> Self {        .opacity            .combined(with: .asymmetric(                insertion: .move(                    edge: up ? .bottom : .top                ),                removal: .move(                    edge: up ? .top : .bottom                )            ))            .combined(with: .scale(scale: 0.7))    }}

Next is the bookkeeping. We can create a Counter view with a simple interface: it takes in the value as an integer. Internally, it remembers the previous value through theState property. Whenever the value changes, the view can compare the new value with the previous one to determine the appropriate transition. It first sets the direction, and in the next transaction (wrapped in a withAnimation) it animates the change.

Code
struct Counter: View {    @State private var up = false    @State private var displayValue = 0        let value: Int        var body: some View {        HStack(spacing: 0) {            ForEach(digits) { digit in                Text(String(digit.character))                    .monospacedDigit()                    .transition(.digit(up: up))            }        }        .onAppear {            displayValue = value        }        .onChange(of: value) { _, newValue in            up = newValue > displayValue            withAnimation(.default) {                displayValue = newValue            }        }    }        private var digits: [CounterDigit] {            }}
Preview
Loading MiniSwiftUI…