Blog

Latest Post

SwiftUI: Shake Animation

Last week, we linked to Robert Böhnke’s Animations Explained, an article from Issue 12. Among other things, it shows a shake animation.

When we first began building animations in SwiftUI, it wasn’t obvious how to achieve certain effects. How, for example, would you create a shake animation similar to Robb’s?

2019 10 01 shake animation

Most animations in SwiftUI are set up implicitly: we change the value of our view’s attributes, and tell SwiftUI that we want this change animated. In this case, however, we don’t really want to change the model; the text field should stay in the same place, it should just shake a little.

As a first attempt, we might try something like:

struct Wrong: View {
    @State var wrongAttempt: Bool = false

       var body: some View {
           VStack {
            Rectangle()
                .fill(Color.pink)
                .frame(width: 200, height: 100)
                .offset(x: wrongAttempt ? -10 : 0)
                .animation(Animation.default.repeatCount(5))
               Spacer()
               Button(action: { self.wrongAttempt.toggle() }, label: { Text("Login") })
           }
       }
}

Here, a boolean set to true triggers an animation, which moves the view by 10 points, and repeats that animation 5 times. As the struct’s name might suggest, there is a problem with the code. After the first animation the button’s x position will be 10 points to the left, and after the next animation it will be correct again.

Instead of animating our model, we can use a modifier. To be precise, we’ll use a GeometryEffect that transforms the view’s x position based on a counter, which we expose as animatableData:

struct Shake: GeometryEffect {
    var amount: CGFloat = 10
    var shakesPerUnit = 3
    var animatableData: CGFloat

    func effectValue(size: CGSize) -> ProjectionTransform {
        ProjectionTransform(CGAffineTransform(translationX:
            amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)),
            y: 0))
    }
}

To use this, we have to modify our code slightly to track the number of attempts. By increasing this number, the animatable data of our Shake modifier will increase by 1. SwiftUI will interpolate between the old and new value, and animate that change.

In the example above, x will animate from 0 to 10, back to -10, back to 10, and stop at 0 (based on shakesPerUnit = 3 and amount = 10).

struct Correct: View {
    @State var attempts: Int = 0

    var body: some View {
        VStack {
            Rectangle()
                .fill(Color.pink)
                .frame(width: 200, height: 100)
                .modifier(Shake(animatableData: CGFloat(attempts)))
            Spacer()
            Button(action: {
                withAnimation(.default) {
                    self.attempts += 1
                }

            }, label: { Text("Login") })
        }
    }
}

We explore animations in more detail in Swift Talk’s SwiftUI Collection. For other interesting animations using GeometryEffect, we recommend The SwiftUI Lab’s article, Advanced SwiftUI Animations — Part 2: GeometryEffect.

To learn with us as we experiment, become a Subscriber.

Previous Posts

SwiftUI: Animating Timing Curves

When you animate a property in SwiftUI, the value gets interpolated using an interpolation function, represented by the Animation type. For example, if you animate from x = 200 to x = 400 using a 1-second linear animation, the value gets interpolated linearly. After half a second, the value will be 300, and after .75 seconds it will be 350.

For a more natural animation, an easeInOut timing function is often used. This is an animation that starts slowly, gets progressively faster towards the halfway point, and then slows down again. This is reminiscent of how objects in the real world behave: as you apply a force to an object, the velocity increases, overcoming inertia; when you stop applying the force, the velocity decreases due to friction.

We wanted to know what the timing curves were for all the built-in animations. To plot them, we used something of a hack: a GeometryEffect with a single CGFloat value that gets animated from 0 to 1. Anytime SwiftUI sets that value, we record the value and the corresponding time, and save that into an array. Then we simply create a number of different animations, animate a random view using the custom geometry effect, and plot out all those values.

Here’s the result:

Animation Timing Curves

It’s interesting to see that the default animation is indeed an .easeInOut with a duration of 0.35 seconds, and how the interpolating spring overshoots and then comes back. The non-auto-reversing repeat animation seems strange at first, but it turned out to be useful for us in episode 165, where we showed how to animate along a path.

The code we used to generate this is available as a gist.

For more reading on animations, check out our issue on animations from 2014 — it still contains a lot of relevant advice! In particular, the Animations Explained article by Robert Böhnke goes into detail about animation curves. Another interesting read is the Advanced SwiftUI Animations series by The SwiftUI Lab, which shows many different ways to implement animations in SwiftUI.

Our SwiftUI Collection contains over five hours of video and growing. To support our experiments, and learn with us, subscribe here.

Animating Explicit Changes

In SwiftUI, there are multiple ways to animate changes to your views. In Swift Talk Episode 165, we showed how to animate a shape along a path, by building a custom shape that conforms to the Animatable protocol.

Here’s the relevant animation code:

struct ContentView: View {
    @State var position: CGFloat = 0
    // ...
    var body: some View {
        VStack {
            ZStack {
                // ...
                OnPath(shape: rect, pathShape: Eight(), offset: position)
                    .foregroundColor(.black)
                    .animation(Animation.linear(duration: 5).repeatForever(autoreverses: false))
            }
            .onAppear(perform: {
                self.position = 1
            })
            // ...
        }
    }
}

We wanted to animate the position from 0 to 1 whenever the view appears, and repeat this animation forever, with the shape animating along the path indefinitely. When we run the application, the animation looks like this:

However, there’s a problem with the code. When we rotate the device, it animates in a very strange way:

Currently, our OnPath shape gets animated implicitly whenever anything it depends on changes: the position, its container size, and so forth. To fix this problem, we need to make sure that our animation only applies to changes to the position property.

Instead, we should rewrite our code like this:

struct ContentView: View {
    @State var position: CGFloat = 0
    // ...
    var body: some View {
        VStack {
            ZStack {
                // ...
                OnPath(shape: rect, pathShape: Eight(), offset: position)
                    .foregroundColor(.black)

            }
            .onAppear(perform: {
                withAnimation(Animation.linear(duration: 5).repeatForever(autoreverses: false)) {
                    self.position = 1
                }
            })
            // ...
        }
    }
}

Now, we’re only adding the repeating path animation when the position changes, which only happens when the view appears. Rotating the device no longer influences the infinite animation.

If you’d like to learn alongside us as we experiment with SwiftUI, we have a growing Swift Talk Collection, with a new episode added every week. Episodes 156: A First Look at SwiftUI, 158: The Swift Talk App, and 167: Building a Collection View are free to watch.

To watch the entire SwiftUI collection, subscribe here.

Defunctionalization

In recent years, the reducer pattern has become more popular in Swift. We see it in UIKit apps, and now in SwiftUI apps.

Consider a simple counter in SwiftUI:

struct ContentView: View {
    @State var count = 0
    var body: some View {
        VStack {
            Text("You clicked \(count) times")
            Button(action: {
                self.count += 1
            }, label: { Text("Click Me") })
            Button(action: {
                self.count = 0
            }, label: { Text("Reset") })
        }
    }
}

For a small program like the one above, the state and its mutations are easy to grasp. There are two mutations: add one to the state, and reset the state to zero. As the size of your program grows, and mutations proliferate through your views, reducer-based refactoring becomes more appealing.

In a reducer-based style, you look at all the different mutations in your program, and create an enum with one case per mutation. For example, refactoring the program above, we could write the following enum:

enum Action {
    case add(Int)
    case reset
}

The second part to a reducer is the “reduce” function (the name is a bit confusing, as it doesn’t have much to do with reduce in the Swift Standard Library). We’ll write this as a mutating function apply on the state:

extension Int {
    mutating func apply(_ action: Action) {
        switch action {
        case .add(let number): self += number
        case .reset: self = 0
        }
    }
}

The technique of taking a program and replacing higher-order functions, such as the button’s actions, with enum cases is called defunctionalization, a term coined by John Reynolds, the inventor of the technique, in his 1972 paper Definitional interpreters for higher-order programming languages.

Interestingly, we can use a similar pattern for something very different: representing links in a web application. In our Swift Talk backend, we have a file called Routes.swift which contains a Route enum. We have a function to turn a Route value into a regular link (for example, Route.collection("swiftui") becomes /collections/swiftui), as well as a function that tries to turn a link into a Route value. Instead of an apply function like above, we have a function called interpret which takes a Route, executes any relevant mutations, and renders the resulting HTML.

This pattern—a Route enum combined with the interpret function—is very similar to the reducer above; it is another type of defunctionalized program. In the case of a web server, its utility doesn’t just lie in taming complexity, it makes it possible to describe continuations in a type-safe way (in this case, as an enum case with associated values, rather than a stringly typed link).

Defunctionalizing a program is an almost mechanical task:

  • Create an Action enum (or Route, or whatever is a good name for your domain).
  • Create an apply function that switches over the enum.
  • For each function that you want to defunctionalize, add a case to the enum, and replace the function with a call to apply. If there are any free variables, add associated values to the enum case. Finally, move the original code into the apply function.

While defunctionalization can be done automatically by a compiler, we typically defunctionalize only part of a program, such as the actions in a GUI, or the web server routes.

Defunctionalization isn’t limited to routes and UI actions:

  • In the Swift Talk backend, we have a ScheduledTask enum that represents a task to be executed at a later time. This enum value is stored in the database along with the scheduled date, and the task gets executed when it’s due.
  • Danvy and Nielsen, in their 2001 paper Defunctionalization at Work, show that we can use defunctionalization to transform continuation based programs into stack-based programs. For example, they turn a functional parser into a state-based parser, much as you would write in C.

We use this technique throughout our Swift Talk episodes, often without making it explicit. As an exception, Episode 62 discusses the reducer pattern in application, using it to improve the testability of a typical view controller.

To learn more about our Swift Talk backend, our introduction episode is free to watch. We recently removed most of our dependencies, including the entire Javascript build stack. The backend is open-source, and you can read the source code on GitHub.

SwiftUI: Paths vs. Shapes

In SwiftUI, there are two similar ways to draw shapes:

  • A Path is a struct that contains the outline of a shape. It’s very similar to the CGPath/CGMutablePath classes from Core Graphics.
  • A Shape is a protocol with a single requirement: a method path(in rect: CGRect) -> Path.

A path is a list of lines, curves and other segments that all have absolute positions. A shape doesn’t know its size up front: the framework calls path(in:) once the final size is known. While a path is absolute, a shape can choose to adjust its path to the given rect.

To draw a resizable vector asset in code, we can create a Shape, and then use the given rect to draw an absolute path. Let’s start with the code for an absolutely-sized balloon-like path:

let balloon = Path { p in
    p.move(to: CGPoint(x: 50, y: 0))
    p.addQuadCurve(to: CGPoint(x: 0, y: 50),
                   control: CGPoint(x: 0, y: 0))
    p.addCurve(to: CGPoint(x: 50, y: 150),
               control1: CGPoint(x: 0, y: 100),
               control2: CGPoint(x: 50, y: 100))
    p.addCurve(to: CGPoint(x: 100, y: 50),
               control1: CGPoint(x: 50, y: 100),
               control2: CGPoint(x: 100, y: 100))
    p.addQuadCurve(to: CGPoint(x: 50, y: 0),
                   control: CGPoint(x: 100, y: 0))
}

balloon.boundingRect // (0, 0, 100, 150)

Interestingly, because Path conforms to View, we can directly draw this in a SwiftUI application:

struct ContentView: View {
    var body: some View {
        balloon
    }
}

2019 08 20 01

By default, the path is filled with the current foreground color. Like a CGPath, we can also apply an affine transform to the path, giving us a new Path:

let balloon2x: Path = balloon.applying(CGAffineTransform(scaleX: 2, y: 2))
balloon2x.boundingRect // (0, 0, 200, 300)

2019 08 20 02

By using the Shape protocol, we can have much more control over paths. For example, we could make a Balloon shape that automatically fills the entire rect (for simplicity, we ignore the rect’s origin):

struct Balloon: Shape {
    func path(in rect: CGRect) -> Path {
        let bounds = balloon.boundingRect
        let scaleX = rect.size.width/bounds.size.width
        let scaleY = rect.size.height/bounds.size.height
        return balloon.applying(CGAffineTransform(scaleX: scaleX, y: scaleY))
    }
}

2019 08 20 03

You can easily turn this into a more generic Fit struct that takes the path as a parameter.

The other APIs are very similar between Path and Shape. For example, we can stroke a Path or a Shape, which gives us back either a new Path or a new Shape, respectively. The moment we apply a fill to the path or shape, the resulting type is some View, which means that we “lose” the fact that we were dealing with a path or shape. For example, it’s not possible to apply a fill and then a stroke, as stroke is only available on paths and shapes.

2019 08 20 04

In conclusion: paths and shapes are almost the same. When you need an absolute drawing, you can use either one, but when you want to adjust the path to the available size, use a Shape.

Swift Talk Episode 164 explores SwiftUI paths and shapes in more depth, as we build an animated loading indicator for our Swift Talk app. The first episode is free to watch.

To watch the entire SwiftUI collection, subscribe here.