How it works
AnimatableModifier is a view modifier protocol whose values SwiftUI can interpolate frame by frame, letting you animate effects that ordinary modifiers can't reach. A standard modifier only changes a view's appearance at fixed states; by conforming to AnimatableModifier you expose a continuous quantity through animatableData, and SwiftUI drives that value smoothly between its start and end. Reach for it when you need custom, parameterized motion — a shake, a wiggle, a counting number, a morphing path — that should respond to withAnimation like any built-in modifier. It combines the rendering of ViewModifier with the interpolation contract of Animatable in a single type.
Conform a modifier to AnimatableModifier
Declare a type that adopts
AnimatableModifier, which inherits from bothViewModifierandAnimatable. In the example,ShakeEffectis a struct conforming toAnimatableModifier, so SwiftUI treats it as a renderable modifier whose state it is also allowed to animate.Expose the animated quantity through animatableData
The
animatableDataproperty is the single value SwiftUI reads and writes as an animation progresses; its type must be aVectorArithmeticsuch asCGFloat.ShakeEffectbacksanimatableDatawith its storedshakesproperty — its getter returnsshakesand its setter assignsnewValue— so each interpolated value flows straight back into the modifier.Render from the current value in body(content:)
Implement
body(content:)to build the modified view fromcontent, the view the modifier is attached to. Because it is re-evaluated for every interpolatedanimatableData, reading the live value produces motion: herecontent.offset(x: sin(shakes * .pi * 2) * 10)shifts the view horizontally, and asshakessweeps upward the sine term oscillates to create the side-to-side shake.Apply it with .modifier(_:)
Attach the modifier to a view with the
.modifier(_:)method, passing a configured instance. The example applies.modifier(ShakeEffect(shakes: shakes))to theText, wiring the modifier's value to a piece of view state so the effect tracks that state.Drive the value inside withAnimation
Changing the bound value inside a
withAnimationblock is what tells SwiftUI to interpolateanimatableDatarather than jump. TheAnimatebutton callswithAnimation(.linear(duration: 0.6)) { shakes += 2 }, so@Stateshakesadvances over time and SwiftUI re-runsbody(content:)at each step to play the animation.
* 10 in content.offset(x: sin(shakes * .pi * 2) * 10) to * 40 to see how animatableData scales the motion it drives.Example & preview
Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.
struct AnimatableModifierDemo: View {
struct ShakeEffect: AnimatableModifier {
var shakes: CGFloat
var animatableData: CGFloat {
get { shakes }
set { shakes = newValue }
}
func body(content: Content) -> some View {
content.offset(x: sin(shakes * .pi * 2) * 10)
}
}
@State private var shakes: CGFloat = 0
var body: some View {
VStack(spacing: 24) {
Text("Shake me!")
.font(.title2)
.padding()
.background(Color.blue.opacity(0.2))
.cornerRadius(12)
.modifier(ShakeEffect(shakes: shakes))
Button("Animate") {
withAnimation(.linear(duration: 0.6)) {
shakes += 2
}
}
}
.padding()
}
}