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:
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) }}
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.
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) }}
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):
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:
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) }}
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:
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) }}
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.
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.
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] { }}