Animatable in SwiftUI Explained - Complete Guide with Examples & Deep Dive

March 28, 2026

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.


What is 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.


Why Does 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:

  • SwiftUI captures the initial value
  • Interpolates intermediate values
  • Recomputes the view on each frame

Without Animatable, changes are treated as instant jumps.

πŸ’‘ SwiftUI animates data, not views.


How SwiftUI Uses Animatable

Every animation follows this pipeline:

State Change
    ↓
withAnimation
    ↓
animatableData
    ↓
Interpolated values
    ↓
View recomputed
    ↓
Rendered frame

Each frame is a fresh evaluation of your body.


Conforming to Animatable

To make a custom type animatable:

  1. Conform to Animatable (or Shape)
  2. Expose animatable values via animatableData
  3. Let SwiftUI interpolate those values

Example 1: Animating a Custom Shape (Corner Radius)

import 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:

What is 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: CGFloat

You are explicitly saying:

  • The animation should happen on cornerRadius
  • SwiftUI can interpolate it as a numeric value

Why Does animatableData Exist?

SwiftUI animations are generic and type agnostic.

The framework does not know:

  • What your custom properties represent
  • Which ones should animate
  • How to interpolate them

So you must expose animatable values through animatableData.

How SwiftUI Uses animatableData

When this code runs:

withAnimation {
    radius = 60
}

SwiftUI performs the following steps:

  • Capture Initial Value
cornerRadius = 0
  • Determine Target Value
cornerRadius = 60
  • Interpolate Using animatableData
0 β†’ 8 β†’ 16 β†’ 24 β†’ 32 β†’ 48 β†’ 60
  • Update Your Shape

For each frame:

cornerRadius = newValue
  • Recompute the Path
path(in:)

Why Getter and Setter Matter

get { cornerRadius }
set { cornerRadius = newValue }
  • Getter β†’ gives SwiftUI the current value
  • Setter β†’ allows SwiftUI to inject interpolated values

Without the setter, your shape would never update during animation.

What Happens If You Remove animatableData?

If you remove this:

var animatableData: CGFloat {
    get { cornerRadius }
    set { cornerRadius = newValue }
}

SwiftUI loses the ability to:

  • Interpolate values
  • Generate intermediate frames

Result:

0 β†’ 60 (instant jump, no animation)

πŸ’‘ animatableData is the bridge between your state and SwiftUI’s animation engine.


Example 2: Animating Multiple Values with AnimatablePair

import 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:

What is 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.

Why Does AnimatablePair Exist?

SwiftUI’s animation system expects one animatable value:

var animatableData: SomeVectorArithmetic

But real-world UI often depends on multiple values:

  • A wave needs both amplitude and frequency
  • A rectangle might need width and height
  • A transform might need scale and rotation

AnimatablePair solves this by acting as a container that SwiftUI can still interpolate.

How SwiftUI Uses AnimatablePair

Internally, 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)

Why the Setter is Critical

set {
    amplitude = newValue.first
    frequency = newValue.second
}

This ensures that:

  • Interpolated values from SwiftUI
  • Are written back into your properties
  • And used inside path(in:)

Without this, animation will not update correctly.

πŸ’‘ AnimatablePair allows SwiftUI to treat multiple values as a single animatable vector.


Example 3: Animating Complex Data with VectorArithmetic

import 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:

What is VectorArithmetic?

VectorArithmetic is a protocol that defines how values behave like mathematical vectors.

It enables SwiftUI to:

  • Add values (+)
  • Subtract values (-)
  • Scale values (scale(by:))
  • Measure distance (magnitudeSquared)

Why Does SwiftUI Require VectorArithmetic?

Animation is fundamentally interpolation between values.

SwiftUI performs operations like:

current + (target - current) * progress

To do this generically, SwiftUI needs:

  • Arithmetic operations
  • Consistent scaling
  • Distance calculation

That’s exactly what VectorArithmetic provides.

How It Applies to This Example

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]

Why Custom Implementation is Needed

SwiftUI does not know how to animate:

[CGFloat]

So you define:

  • How values combine (+)
  • How they differ (-)
  • How they scale (scale)

This makes your type animatable.

πŸ’‘ VectorArithmetic turns your data into a multi-dimensional space that SwiftUI can animate through.


Example 4: Animating a Progress Ring

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 β†’ 1

Each frame increases the arc angle:

Angle = 360 Γ— progress

πŸ’‘ The ring is just a visual representation of a single number.


Final Mental Model

All animations follow this pipeline:

State Change
    ↓
withAnimation
    ↓
animatableData
    ↓
Interpolated values
    ↓
View recomputed
    ↓
New frame rendered

Key Takeaways

  • SwiftUI animations are data-driven
  • Animatable defines what can interpolate
  • Each frame recomputes your view
  • AnimatablePair handles multiple values
  • VectorArithmetic enables complex animations

Final Thoughts

πŸ’‘ 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.