Blog

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.


  • See the Swift Talk Collection

Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts