NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Animating complex shapes with AnimatablePair

Paul Hudson    @twostraws   

SwiftUI uses an animatableData property to let us animate changes to shapes, but what happens when we want two, three, four, or more properties to animate? animatableData is a property, which means it must always be one value, however we get to decide what type of value it is: it might be a single CGFloat, or it might be two values contained in a special wrapper called AnimatablePair.

To try this out, let’s look at a new shape called Checkerboard, which must be created with some number of rows and columns:

struct Checkerboard: Shape {
    var rows: Int
    var columns: Int

    func path(in rect: CGRect) -> Path {
        var path = Path()

        // figure out how big each row/column needs to be
        let rowSize = rect.height / CGFloat(rows)
        let columnSize = rect.width / CGFloat(columns)

        // loop over all rows and columns, making alternating squares colored
        for row in 0..<rows {
            for column in 0..<columns {
                if (row + column).isMultiple(of: 2) {
                    // this square should be colored; add a rectangle here
                    let startX = columnSize * CGFloat(column)
                    let startY = rowSize * CGFloat(row)

                    let rect = CGRect(x: startX, y: startY, width: columnSize, height: rowSize)

        return path

We can now create a 4x4 checkerboard in a SwiftUI view, using some state properties that we can change using a tap gesture:

struct ContentView: View {
    @State private var rows = 4
    @State private var columns = 4

    var body: some View {
        Checkerboard(rows: rows, columns: columns)
            .onTapGesture {
                withAnimation(.linear(duration: 3)) {
                    self.rows = 8
                    self.columns = 16

When that runs you should be able to tap on the black squares to see the checkerboard jump from being 4x4 to 8x16, without animation even though the change is inside a withAnimation() block.

As with simpler shapes, the solution here is to implement an animatableData property that will be set with intermediate values as the animation progresses. Here, though, there are two catches:

  1. We have two properties that we want to animate, not one.
  2. Our row and column properties are integers, and SwiftUI can’t interpolate integers.

To resolve the first problem we’re going to use a new type called AnimatablePair. As its name suggests, this contains a pair of animatable values, and because both its values can be animated the AnimatablePair can itself be animated. We can read individual values from the pair using .first and .second.

To resolve the second problem we’re just going to do some type conversion: we can convert a Double to an Int just by using Int(someDouble), and go the other way by using Double(someInt).

So, to make our checkerboard animate changes in the number of rows and columns, add this property:

public var animatableData: AnimatablePair<Double, Double> {
    get {
       AnimatablePair(Double(rows), Double(columns))

    set {
        self.rows = Int(newValue.first)
        self.columns = Int(newValue.second)

Now when you run the app you should find the change happens smoothly – or as smoothly as you would expect given that we’re rounding numbers to integers.

Of course, the next question is: how do we animate three properties? Or four?

To answer that, let me show you the animatableData property for SwiftUI’s EdgeInsets type:

AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>>>

Yes, they use three separate animatable pairs, then just dig through them using code such as newValue.second.second.first.

I’m not going to claim this is the most elegant of solutions, but I hope you can understand why it exists: because SwiftUI can read and write the animatable data for a shape regardless of what that data is or what it means, it doesn’t need to re-invoke the body property of our views 60 or even 120 times a second during an animation – it just changes the parts that actually are changing.

Hacking with Swift is sponsored by Instabug

SPONSORED Are you tired of wasting time debugging your Swift app? Instabug’s SDK is here to help you minimize debugging time by providing you with complete device details, network logs, and reproduction steps with every bug report. All data is attached automatically, and it only takes a line of code to setup. Start your free trial now and get 3 months off exclusively for the Hacking with Swift Community.

Start your free trial!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.2/5

Link copied to your pasteboard.