• Aug 17, 2025

Animatable Macro in iOS 26: Simplifying Shape Animations in SwiftUI

Starting with iOS 26, making properties animatable has become easier than ever with the new @Animatable macro. But before we dive into the solution, let’s first understand the problem it solves.

Imagine you’re building a custom shape. To keep things simple, let’s start with something familiar — a circle.

struct CircleShape: Shape {
    var radius: CGFloat
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addArc(center: .init(x: rect.midX, y: rect.midY),
                        radius: radius,
                        startAngle: .zero,
                        endAngle: .init(degrees: 360),
                        clockwise: false)
        }
    }
}

Next, let’s place this circle inside a view. When the user taps on it, we’ll change its radius so the circle smoothly expands outward.

struct ContentView: View {
    @State private var expand: Bool = false
    
    var body: some View {
        VStack {
            CircleShape(radius: expand ? 100 : 10)
                .contentShape(.rect)
                .onTapGesture {
                    withAnimation(.smooth) {
                        expand.toggle()
                    }
                }
        }
        .padding()
    }
}

You’ll notice that the circle isn’t animating yet. The reason is that, by default, SwiftUI doesn’t know how to interpolate between changing values. We can fix this by adding an animatableData computed property to our shape.

Since we want the radius to animate, we’ll expose it through animatableData, allowing SwiftUI to smoothly transition between values.

struct CircleShape: Shape {
    var radius: CGFloat
    
    var animatableData: Double {
        get { radius }
        set { radius = newValue }
    }
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addArc(center: .init(x: rect.midX, y: rect.midY),
                        radius: radius,
                        startAngle: .zero,
                        endAngle: .init(degrees: 360),
                        clockwise: false)
        }
    }
}