Improving Animations on iOS with Stagehand

Adding animations to your app turns a fairly routine interaction into a more enjoyable, exciting experience. These animations make a straightforward design into something that feels polished and professional.

Unfortunately, the experience of implementing these animations isn’t always as enjoyable as the final product. Animations have been an integral part of iOS since the beginning, with the CoreAnimation framework being available to developers in the first public release of the platform. The iOS ecosystem has come a long way since then. Swift brought more compile-time safety into the development process. Testing views became more straightforward with the introduction of snapshot testing. Apple’s in-house designed chips brought previously unthinkable performance to handheld devices. Building animations have come a long way—but remnants of the old world are still around.

A (Simplified) History of Animations on iOS

From the beginning, layers were animatable using CABasicAnimation and CAKeyframeAnimation (which makes sense, after all the CA prefix refers to Core_Animation_). Then in iOS 3.0, Apple exposed animation-specific methods on CATransaction that allowed batching layer-tree operations together so animations could be built up in transactions.

In iOS 4.0, Apple moved the animation API up a level from CoreAnimation into UIKit. The move into UIKit also came with a change in how the animations were defined. Gone were the stringly-typed key paths and type-erased values of the C world, with Objective-C blocks taking their place. Unfortunately, animations from UIKit and CoreAnimation don’t always play nicely together, so developers still have to jump through hoops to make complex animations.

iOS 7.0 brought some great advancements to UIView animations with the introduction of keyframes animations and spring timing curves, as well as the ability to customize animations transitions between view controllers. Another big change came in iOS 10.0 with the introduction of UIViewPropertyAnimator. Property animators move towards a greater separation of construction and execution, although they still require having access to the instance being animated during construction.

Today in iOS 13.0, improvements to animations APIs are mainly focused on SwiftUI, leaving UIKit animations limited to the APIs previously mentioned. While these APIs give a lot of power to developers, they fall short on the conveniences provided by modern programming languages by holding on to remnants of the old world in style, syntax, and compile time safety.

Introducing Stagehand

Today we’re releasing Stagehand, an open source framework that makes building animations on iOS easy. Stagehand provides a modern Swift API for animations complete with compile-time safety, a composable structure, separation of construction and execution, and straightforward testability.

Making Compile-Time Safe Animations

Stagehand uses modern Swift features to provide a compile-time safe API for defining animations. Keyframes are defined using Swift key paths, which guarantees the right value type is provided. The interpolation of property values is controlled via protocol conformance, so custom property types can be animated. Animations are constructed using generic value types, so animations can be applied to more than only views and layers.

Composing Animations

One of the key goals of Stagehand is to provide a composable structure for building up animations.

As an example, say we want to transition between two subviews. We have a ContainerView class that we’ve installed with its subviews accessible via two properties: the fromView and the toView. We want the initial view to fade out over the first half of the animation, then the final view to fade in over the second half. With Stagehand, we can separate out the phases of this animation and then compose them into a parent animation:

func makeSwapAnimation() -> Animation<ContainerView> {
    var animation = Animation<ContainerView>()
        for: \.fromView,
        startingAt: 0,
        relativeDuration: 0.5
        for: \.toView,
        startingAt: 0.5,
        relativeDuration: 0.5
    return animation

func makeFadeOutAnimation() -> Animation<UIView> {
    var animation = Animation<UIView>()
    animation.addKeyframe(for: \.alpha, at: 0, value: 1)
    animation.addKeyframe(for: \.alpha, at: 1, value: 0)
    return animation

func makeFadeInAnimation() -> Animation<UIView> {
    var animation = Animation<UIView>()
    animation.addKeyframe(for: \.alpha, at: 0, value: 0)
    animation.addKeyframe(for: \.alpha, at: 1, value: 1)
    return animation

The ability to compose animations makes it easier to write small components that are easy to reason about, then put them together to make complex, multi-part animations. This also makes it possible to reuse flexible animation components across many different animations.

Separating Construction and Execution

Another key principle in Stagehand is the separation of construction and execution. In our example above we defined what our animation does but we didn’t animate anything with it. In fact, we haven’t even created our view to animate!

let animation = makeSwapAnimation()

let containerView = ContainerView(...)

animation.perform(on: containerView)

Separating the construction and execution of animations increases the flexibility of animations, as well as making concepts like queuing a series of animations work straight out of the box.

Testing Animations

Stagehand builds on the concept of snapshot testing to introduce a visual testing paradigm for animations. Using a set of simple methods, you can now write tests to verify your animations using either snapshots of specific keyframes or an animated GIF of the entire animation.

A snapshot image from the Cash App codebase, showing an animated transition between two screens.

With snapshot testing in Stagehand, animation regressions are a thing of the past.

Using Stagehand

Stagehand is available on GitHub. Check out the included demo app and have fun writing animations!