SwiftUI Field Guide

Animatable

In SwiftUI, we can rely on the built-in animatable properties of nodes. However, if we want to hook into the animation system ourselves, we can do so through the Animatable protocol, or in modern SwiftUI using the @Animatable macro. The macro hooks the animatable stored properties of a view or view modifier into the animation system.

Animating Numbers

For example, in this counter, if we animate an update to the Counter struct, SwiftUI will interpolate the value property for us because we marked our struct with @Animatable. We store the value as a Double and display it as an Int.

Code
@Animatablestruct Counter: View {    var value: Double        var body: some View {        Text("\(Int(value))")            .monospacedDigit()    }}struct CounterExample: View {    @State private var value = 0.0        var body: some View {        Counter(value: value)            .frame(maxWidth: .infinity,                   maxHeight: .infinity)            .contentShape(.rect)            .onTapGesture {                withAnimation(.linear(                                  duration: 2                              )) {                    value = value == 0 ? 100 : 0                }            }    }}
Preview
Loading preview…
Attribute Graph (simplified)
Counter attribute graphThe graph will show values when the Swift preview loads.CounterExample.bodyCounter.bodyThe stored state changes to the destination on activation.@State value: —The Counter declaration supplied by the parent body.Counter(value: —)Animation time. The edge exists only while the animatable attribute depends on time.— sTimeInterpolated Counter.value and retained animation, read from the mounted animatable attribute.Animatable: —The Text returned by Counter.body. Its integer can stay the same across body evaluations.Text(—)
0.00 s

In the simplified attribute graph, we can see that changing the state property causes an update to propagate through its dependent nodes. When the update reaches the animatable node, SwiftUI uses the animation in the transaction to start animating its value and adds a dependency on the current animation time.

While the animation is running, this dynamic dependency causes SwiftUI to interpolate value and evaluate Counter.body as time changes. When the animation finishes, SwiftUI removes the dependency on time. This is what makes our counter animate.

Wiggle

To get full control of our animations, it's often easiest to increment a value by one and use it as progress. That's exactly what we do in this wiggle example. Our Wiggle modifier then uses a sine wave to turn that progress into a rotation.

Code
@Animatablestruct Wiggle: ViewModifier {    var progress: Double        func body(content: Content) -> some View {        content            .rotationEffect(.degrees(sin(                                         progress * 4 * .pi                                     ) * 15))    }}struct WiggleExample: View {    @State private var value = 0.0        var body: some View {        RoundedRectangle(cornerRadius: 12)            .fill(.red)            .frame(width: 80, height: 96)            .modifier(Wiggle(progress: value))            .frame(maxWidth: .infinity,                   maxHeight: .infinity)            .contentShape(.rect)            .onTapGesture {                withAnimation(.linear(                                  duration: 1                              )) {                    value += 1                }            }    }}
Preview
Loading preview…
Track
Animatable wiggle angle over time-3003000.51Time (s)

Drag the playhead or use the Animation time slider to inspect the animation.

0.00 s

As we scrub through the track, we can see exactly how this drives our animation. On the first tap, the state property changes directly from 0 to 1, with no values in between. Because we marked Wiggle with @Animatable, SwiftUI interpolates its progress property between the old and new values. Those intermediate values drive the wiggle.

We can also build a wiggle using phase animations or keyframe animations.

Along a Path

We can also get more creative. For example, here we use the progress to find a point on a path and position our view at that point. This uses the exact same technique as the wiggle animation, just with a different effect.

Code
@Animatablestruct FollowPath: ViewModifier {    var progress: Double    let path: Path        func body(content: Content) -> some View {        let point = path            .trimmedPath(from: 0, to: progress)            .currentPoint ?? .zero        content            .position(point)    }}struct FollowPathExample: View {    @State private var value = 0.0        var body: some View {        let path = Path { path in            path.move(to: .zero)            path.addCurve(to: CGPoint(x: 140,                                      y: 100),                          control1: CGPoint(x: 0,                                            y: 180),                          control2: CGPoint(                              x: 140,                              y: -80                          ))        }        Circle()            .fill(.red)            .frame(width: 24, height: 24)            .modifier(FollowPath(progress: value,                                 path: path))            .background {                path.stroke(.tertiary,                            lineWidth: 2)            }            .frame(width: 140, height: 100)            .frame(maxWidth: .infinity,                   maxHeight: .infinity)            .contentShape(.rect)            .onTapGesture {                withAnimation(.linear(                                  duration: 3                              )) {                    value = value == 0 ? 1 : 0                }            }    }}
Preview
Loading preview…
0.00 s

The progress value we pass to trimmedPath(from:to:) isn't exactly proportional to the distance along the path. This means our view won't move at a perfectly constant speed, but it's close enough for this example.

Resources

Share Image

Thinking in SwiftUI, Animations chapter: “The Animatable Protocol,” including the custom opacity modifier and sine-based Shake example. Related sections: “Timing Curves” and “Transactions.”

https://www.objc.io/books/thinking-in-swiftui/
Share Image

Swift Talk #402, Wobble Animation. The Animatable modifier starts at 8:47, with sine-based rotation at 10:47.

https://talk.objc.io/episodes/S01E402-wobble-animation
Share Image

Swift Talk #165, Animating along Paths, moves a shape along a path using animatable data. #166, Geometry Effects, adds rotation along the path.

https://talk.objc.io/episodes/S01E165-animating-along-paths
Share Image

Four Ways to Shake compares a custom Animatable view with phase, keyframe, and timeline animations, including their velocity curves.

https://chris.eidhof.nl/post/four-ways-to-shake/
Share Image

Apple's What's new in SwiftUI (WWDC25): @Animatable and @AnimatableIgnored at 11:24.

https://developer.apple.com/videos/play/wwdc2025/256/?time=684