SwiftUI provides a powerful, declarative animation system that enables smooth, expressive user interfaces. In most cases, animations can be achieved using built in APIs like withAnimation or .animation(_:).
However, when you need precise control over how values change over time, SwiftUI exposes a deeper mechanism: the Animatable protocol.
This article explores how Animatable works, why it matters, and how to use it to build custom, data-driven animations.
Animatable?Animatable is a protocol that allows a type to define which of its values should be interpolated during animation.
var animatableData: Self.AnimatableData { get set }The associated type must conform to VectorArithmetic, which enables SwiftUI to perform interpolation between values.
Animatable Matter?SwiftUI does not animate views directly. Instead, it animates data, and views are recomputed from that data.
When a value changes inside a withAnimation block:
Without Animatable, changes are treated as instant jumps.
π‘ SwiftUI animates data, not views.
AnimatableEvery animation follows this pipeline:
State Change
β
withAnimation
β
animatableData
β
Interpolated values
β
View recomputed
β
Rendered frameEach frame is a fresh evaluation of your body.
AnimatableTo make a custom type animatable:
Animatable (or Shape)animatableDataimport SwiftUI
struct RoundedRectangleShape: Shape {
var cornerRadius: CGFloat
var animatableData: CGFloat {
get { cornerRadius }
set { cornerRadius = newValue }
}
func path(in rect: CGRect) -> Path {
Path(roundedRect: rect, cornerRadius: cornerRadius)
}
}
struct CornerRadiusExampleView: View {
@State private var radius: CGFloat = 0
var body: some View {
VStack(spacing: 20) {
RoundedRectangleShape(cornerRadius: radius)
.fill(Color.blue.gradient)
.frame(width: 200, height: 200)
Button("Toggle Radius") {
withAnimation(.easeInOut(duration: 1)) {
radius = radius == 0 ? 60 : 0
}
}
}
.padding()
}
}Output:
animatableData?animatableData is the core property that connects your type to SwiftUIβs animation system.
It tells SwiftUI:
βThese are the values you can interpolate over time.β
In this example:
var animatableData: CGFloatYou are explicitly saying:
cornerRadiusanimatableData Exist?SwiftUI animations are generic and type agnostic.
The framework does not know:
So you must expose animatable values through animatableData.
animatableDataWhen this code runs:
withAnimation {
radius = 60
}SwiftUI performs the following steps:
cornerRadius = 0cornerRadius = 60animatableData0 β 8 β 16 β 24 β 32 β 48 β 60For each frame:
cornerRadius = newValuepath(in:)get { cornerRadius }
set { cornerRadius = newValue }Without the setter, your shape would never update during animation.
animatableData?If you remove this:
var animatableData: CGFloat {
get { cornerRadius }
set { cornerRadius = newValue }
}SwiftUI loses the ability to:
Result:
0 β 60 (instant jump, no animation)π‘
animatableDatais the bridge between your state and SwiftUIβs animation engine.
AnimatablePairimport SwiftUI
struct WaveShape: Shape {
var amplitude: CGFloat
var frequency: CGFloat
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(amplitude, frequency) }
set {
amplitude = newValue.first
frequency = newValue.second
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
for x in stride(from: 0, through: rect.width, by: 1) {
let relativeX = x / rect.width
let y = amplitude * sin(frequency * relativeX * .pi * 2)
if x == 0 {
path.move(to: CGPoint(x: x, y: y + rect.midY))
} else {
path.addLine(to: CGPoint(x: x, y: y + rect.midY))
}
}
return path
}
}
struct WaveExampleView: View {
@State private var amplitude: CGFloat = 10
@State private var frequency: CGFloat = 1
var body: some View {
VStack(spacing: 20) {
WaveShape(amplitude: amplitude, frequency: frequency)
.stroke(Color.blue.gradient, lineWidth: 2)
.frame(height: 200)
Button("Animate Wave") {
withAnimation(.easeInOut(duration: 1)) {
amplitude = amplitude == 10 ? 50 : 10
frequency = frequency == 1 ? 3 : 1
}
}
}
.padding()
}
}Output:
AnimatablePair?AnimatablePair is a built-in SwiftUI type that allows you to combine two animatable values into a single animatable unit.
AnimatablePair<First, Second>Both First and Second must conform to VectorArithmetic.
AnimatablePair Exist?SwiftUIβs animation system expects one animatable value:
var animatableData: SomeVectorArithmeticBut real-world UI often depends on multiple values:
AnimatablePair solves this by acting as a container that SwiftUI can still interpolate.
AnimatablePairInternally, SwiftUI treats AnimatablePair as a 2D vector:
(amplitude, frequency)When animation runs:
(10, 1) β (50, 3)SwiftUI interpolates both dimensions independently:
Frame 1: (10, 1)
Frame 2: (20, 1.5)
Frame 3: (30, 2)
Frame 4: (40, 2.5)
Frame 5: (50, 3)set {
amplitude = newValue.first
frequency = newValue.second
}This ensures that:
path(in:)Without this, animation will not update correctly.
π‘
AnimatablePairallows SwiftUI to treat multiple values as a single animatable vector.
VectorArithmeticimport SwiftUI
struct AnimatableVector: VectorArithmetic {
var values: [CGFloat]
static var zero: AnimatableVector {
AnimatableVector(values: [0, 0, 0])
}
static func + (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
AnimatableVector(values: zip(lhs.values, rhs.values).map(+))
}
static func - (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
AnimatableVector(values: zip(lhs.values, rhs.values).map(-))
}
mutating func scale(by rhs: Double) {
values = values.map { $0 * CGFloat(rhs) }
}
var magnitudeSquared: Double {
values.reduce(0) { $0 + Double($1 * $1) }
}
}
struct BarChartShape: Shape {
var values: AnimatableVector
var animatableData: AnimatableVector {
get { values }
set { values = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let barWidth = rect.width / CGFloat(values.values.count)
for (index, value) in values.values.enumerated() {
let x = CGFloat(index) * barWidth
let height = rect.height * value
path.addRect(
CGRect(
x: x,
y: rect.height - height,
width: barWidth - 6,
height: height
)
)
}
return path
}
}
struct BarChartExampleView: View {
@State private var data = AnimatableVector(values: [0.2, 0.4, 0.3])
var body: some View {
VStack(spacing: 20) {
BarChartShape(values: data)
.fill(Color.blue.gradient)
.frame(height: 200)
Button("Randomize Data") {
withAnimation(.easeInOut(duration: 1)) {
data = AnimatableVector(values: [
.random(in: 0.1...1),
.random(in: 0.1...1),
.random(in: 0.1...1)
])
}
}
}
.padding()
}
}Output:
VectorArithmetic?VectorArithmetic is a protocol that defines how values behave like mathematical vectors.
It enables SwiftUI to:
+)-)scale(by:))magnitudeSquared)VectorArithmetic?Animation is fundamentally interpolation between values.
SwiftUI performs operations like:
current + (target - current) * progressTo do this generically, SwiftUI needs:
Thatβs exactly what VectorArithmetic provides.
Your data:
[0.2, 0.4, 0.3]Target:
[0.8, 0.1, 0.6]SwiftUI interpolates element-wise:
Frame 1: [0.2, 0.4, 0.3]
Frame 2: [0.4, 0.3, 0.4]
Frame 3: [0.6, 0.2, 0.5]
Frame 4: [0.8, 0.1, 0.6]SwiftUI does not know how to animate:
[CGFloat]So you define:
+)-)scale)This makes your type animatable.
π‘
VectorArithmeticturns your data into a multi-dimensional space that SwiftUI can animate through.
import SwiftUI
struct ProgressRing: Shape {
var progress: Double
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let endAngle = Angle(degrees: 360 * progress)
path.addArc(
center: CGPoint(x: rect.midX, y: rect.midY),
radius: rect.width / 2,
startAngle: .degrees(-90),
endAngle: endAngle - .degrees(90),
clockwise: false
)
return path
}
}
struct ProgressRingExampleView: View {
@State private var progress = 0.0
var body: some View {
VStack(spacing: 20) {
ProgressRing(progress: progress)
.stroke(Color.blue.gradient, lineWidth: 12)
.frame(width: 150, height: 150)
Button("Start Animation") {
progress = 0
withAnimation(.linear(duration: 2)) {
progress = 1.0
}
}
}
.padding()
}
}Output:
SwiftUI interpolates:
0 β 0.2 β 0.4 β 0.6 β 0.8 β 1Each frame increases the arc angle:
Angle = 360 Γ progressπ‘ The ring is just a visual representation of a single number.
All animations follow this pipeline:
State Change
β
withAnimation
β
animatableData
β
Interpolated values
β
View recomputed
β
New frame renderedAnimatable defines what can interpolateAnimatablePair handles multiple valuesVectorArithmetic enables complex animationsπ‘ If SwiftUI can interpolate your data, it can animate your UI.
If you have suggestions, feel free to connect with me on X and send me a DM. If this article helped you, Buy me a coffee.