Categories
iOS Swift SwiftUI

Animating with PhaseAnimator in SwiftUI

WWDC’23 introduced us a new view modifiers for animating views in SwiftUI. In this blog post, we are going to have a look at phase animation view modifiers. There are two view modifiers: phaseAnimator(_:content:animation:) and phaseAnimator(_:trigger:content:animation:). The first one creates an animation which runs continuously by cycling through all the animation steps. The latter is triggered once when an observed value has changed.

A multistep or multiphase animation is easiest to model using an enum.

enum BannerAnimationPhase: CaseIterable {
case initial, middle, final
var animation: Animation {
switch self {
case .initial: .easeIn
case .middle: .linear
case .final: .easeOut
}
}
var gradientStartPoint: UnitPoint {
switch self {
case .initial: .bottomLeading
case .middle: .leading
case .final: .topLeading
}
}
var rotation: Angle {
switch self {
case .initial: .degrees(-15)
case .middle: .degrees(15)
case .final: .zero
}
}
var scale: Double {
switch self {
case .initial: 1.5
case .middle: 1.7
case .final: 1.0
}
}
}

Here we have an enum with 3 animation phases. Each of the phase slightly modifies 3 values: scale, rotation and gradient start point. These properties are accessed from the phaseAnimator’s view builder, where we modify the original view based on the phase. In addition, we also configured different animations based on the current phase.

struct ContentView: View {
var body: some View {
BannerView()
.phaseAnimator(BannerAnimationPhase.allCases) { content, phase in
content
.background(LinearGradient(colors: [.orange, .yellow],
startPoint: phase.gradientStartPoint,
endPoint: .trailing))
.cornerRadius(8.0)
.scaleEffect(phase.scale)
.rotationEffect(phase.rotation)
} animation: { phase in
phase.animation
}
}
}

When we only want to have 2 phases for our animation, then we can use boolean values for defining the animation. Below is an example of an animation which is triggered when a value changes and the animation just scales up the banner view and then animates to the initial state – false.

BannerView()
.phaseAnimator([false, true],
trigger: counter) { content, isEnabled in
content
.scaleEffect(isEnabled ? 2.0 : 1.0)
} animation: { isEnabled in
isEnabled ? .bouncy : .easeOut(duration: 0.7)
}
Button("Trigger", action: { counter += 1 })

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.