Building a declarative animation framework in Swift - Part 1

When working with animations in apps, I like to divide them into 3 separate categories; scene-based, view-based and frame-based. A couple of weeks ago, we took a look at how SpriteKit can be a really nice tool for scene-based animations, which is when we're performing animations on elements that are part of a stand-alone scene.

This week, let's take a look at how we can make view-based animations (which is when we're animating individual UIViews, like buttons and labels) on iOS easier to handle - by building a simple framework that will enable us to express animations in a very declarative, composable way (so many buzzwords 😅).

In this post, we will build the initial implementation of the framework, and next week we'll extend it to become even more powerful and flexible. Let's get started 👍

Normal animations

Normally, we build view-based animations on iOS using UIViews animate API, like this:

UIView.animate(withDuration: 0.3) {
    button.frame.size = CGSize(width: 200, height: 200)
}

While that works really well for simple animations (like the case above, where we simply want to animate the resize of a button), things can grow out of hand quite quickly. Let's say that we just want to add a fade-in before we resize our button, we'd have to write something like this:

UIView.animate(withDuration: 0.3, animations: {
    button.alpha = 1
}, completion: { _ in
    UIView.animate(withDuration: 0.3) {
        button.frame.size = CGSize(width: 200, height: 200)
    }
})

What becomes clear is that the more steps we add to our animation, the more complex and hard to read our code becomes. While complex and hard-to-read code is something that we should always try to avoid whenever possible, it's particularly bad for animation code, which is something that usually requires a lot of tweaking and experimentation to make the animations feel "just right" 👌.

Declarative animations

Part of what I really like about declarative APIs and coding styles, is that it can be a great way of making code easier to read. The idea is that rather than specifying all the operations we want to do line-by-line, we declare everything that we're going to do up-front (I'll be writing more about declarative programming techniques for Swift in a future blog post).

Wouldn't it be nice if we could simply declare all of our animation steps, instead of having to write nested closures? That would make the fade-in-then-resize button animation from before look like this:

button.animate([
    .fadeIn(duration: 0.3),
    .resize(to: CGSize(width: 200, height: 200), duration: 0.3)
])

With an API like the above, tweaking our animation (like adding or removing steps, changing durations or other parameters, etc) would be super easy. The code also becomes a lot more readable and easier to reason about, since we no longer have to try to figure out what is going on in an n-level nested closure.

The good news is that creating an API that will let us write animations like the above is not a lot of work - in fact, we're going to write a small framework that enables us to do exactly that, right now! 😀

Let's open up a playground

Whenever I have a new idea for a framework, I always fire up a playground. In fact, most of my open source projects were built almost entirely in a playground. Just like how I love working in playgrounds when doing test driven development, the quick feedback loop you get is super valuable when creating a new framework or new APIs.

In Xcode, select File > New > Playground... to get started (if you have my Playground script installed, you can also just type playground on the command line to quickly create one).

Since we'll be building an animation framework, let's create a live view, that will let us run animations and see the result live. To do this, we import PlaygroundSupport and assign a liveView to the current PlaygroundPage, like this:

import UIKit
import PlaygroundSupport

let view = UIView(frame: CGRect(
    x: 0, y: 0,
    width: 500, height: 500
))

view.backgroundColor = .white

PlaygroundPage.current.liveView = view

The model

To be able to express animations declaratively, we're going to need a model. Let's use a simple struct that contains the duration of an animation, as well as a closure that will be run to actually perform the animation on a UIView:

public struct Animation {
    public let duration: TimeInterval
    public let closure: (UIView) -> Void
}

Now, to enable the nice API that allows us to call view.animate() and an array of animations, we're going to need some factory methods that return Animation values for fade in, resize, etc.

To add those, we create an extension on Animation and add static methods for each kind of animation we want to support. For now, we'll add the methods fadeIn() and resize(to:):

public extension Animation {
    static func fadeIn(duration: TimeInterval = 0.3) -> Animation {
        return Animation(duration: duration, closure: { $0.alpha = 1 })
    }

    static func resize(to size: CGSize, duration: TimeInterval = 0.3) -> Animation {
        return Animation(duration: duration, closure: { $0.bounds.size = size })
    }
}

A really great thing about this solution to animations (and declarative solutions in general), is that it is super easy to extend with new implementations. For example, using the above technique, we can quickly add factory methods that return animations for fadeOut, move, rotate, etc with just a few more lines of code.

One thing you might note in the code above is that we use default argument values for the duration parameters. This is usually a good idea in public facing APIs and frameworks, in order to make the API easier to use for the default case. By doing this, a fade in animation can be performed by simply calling .fadeIn(), without having to specify a duration if the default one should be used.

Animating

Now that we have a model in place, let's start writing the actual animation code. We're going to add two variants of our animation API, one that executes all animations in sequence, and one that executes them in parallel. Both will be added through an extension on UIView, that enables any view subclass to be animated.

Let's start with the sequential animation API, which looks like this:

public extension UIView {
    func animate(_ animations: [Animation]) {
        // Exit condition: once all animations have been performed, we can return
        guard !animations.isEmpty else {
            return
        }

        // Remove the first animation from the queue
        var animations = animations
        let animation = animations.removeFirst()

        // Perform the animation by calling its closure
        UIView.animate(withDuration: animation.duration, animations: {
            animation.closure(self)
        }, completion: { _ in
            // Recursively call the method, to perform each animation in sequence
            self.animate(animations)
        })
    }
}

When performing operations in sequence, it's super nice to use recursion, like we do above. That way, we don't need to maintain a lot of state, have complex loops or nested closures - but instead we can just have a straight forward implementation that calls itself until an exit condition is met (the initial guard statement in our case).

Let's take it for a spin!

Now that we have a model and an API that lets us use that model to animate a sequence of animation values - let's try it out and see how it works!

Let's create a simple UIView to animate (we'll call it animationView), which we will fade in and resize. We'll use a quite long animation duration (3 seconds in this case) to be able to really see the animation in slow-motion in our playground:

let animationView = UIView(frame: CGRect(
    x: 0, y: 0,
    width: 50, height: 50
))

animationView.backgroundColor = .red
animationView.alpha = 0
view.addSubview(animationView)

animationView.animate([
    .fadeIn(duration: 3),
    .resize(to: CGSize(width: 200, height: 200), duration: 3)
])

Open up your playground's Assistant editor (by pressing âŒĨ + ⌘ + ↩ī¸Ž) to reveal a rendering of the live view, and you should now see something like this:

Parallelizing

Sometimes we don't want to perform animations in sequence, but rather all at once - in parallel. So let's add an API for that as well. The implementation of this new parallelizing API is actually a lot simpler, since all it needs to do is to iterate over all the animations and perform their closures at once (rather than sequencing them one after another). It looks like this:

public extension UIView {
    func animate(inParallel animations: [Animation]) {
        for animation in animations {
            UIView.animate(withDuration: animation.duration) {
                animation.closure(self)
            }
        }
    }
}

To see it in action, we can simply add the inParallel: prefix when we call animationView.animate(), like this:

animationView.animate(inParallel: [
    .fadeIn(duration: 3),
    .resize(to: CGSize(width: 200, height: 200), duration: 3)
])

You should now see the following in your live view:

To be continued

We now have a solid foundation for an easy to use, declarative animation framework - which is also super easy to extend, both in the framework itself and in the apps that it will be used in 🎉.

You can find all of the code that we've written up to this point on GitHub. In the next post, we'll continue by extending our animation framework to be able to coordinate animations between multiple views, and sequence them in a nice way.

You can continue reading part 2 of this post here.

Also feel free to comment below or contact me on Twitter if you have any questions or feedback about this post, or any of my other ones.

Finally, if you want to hear me talk more about animations (which I really love), check out the video from my talk "Creating great animations on iOS" from the ADDC conference in June. You can find it on YouTube here.

Thanks for reading 🚀

Building a declarative animation framework in Swift - Part 2

Identifying objects in Swift