Shake Animation with Animatable in SwiftUI

DevTechie Inc
Jan 29, 2023


Shake Animation with Animatable in SwiftUI

Animatable protocol describes how to animate a view with respect to some change in the view’s data. We use animatable when we want to create custom animation.

Animatable protocol gives us fine-grained control over the animation of a SwiftUI view’s animatable values. It does so by requiring animatableData: AnimatableData, which represents a view’s animatable data.

By conforming to Animatable, we are able to decouple the animation of our view from the concept of duration, as we give SwiftUI the ability to interpolate arbitrarily between two different values for animatableData. This is also the reason why AnimatableData must conform to VectorArithmetic, which provides the runtime means to add, subtract and scale the animated values as necessary to generate data points for each frame of the animation over an arbitrary time interval.

We will build custom shake animation by conforming to AnimatableModifier, which is a combination of Animatableand ViewModifier.

Let’s start with a struct which will conform to AnimatableModifier protocol.

struct Shake: AnimatableModifier {
    
}
Next, we will create body property along with a variable to take number of shakes as input and animatableDatacomputed property to get and set the value of number of shakes.

struct Shake: AnimatableModifier {
    var shakes: CGFloat = 0
    
    var animatableData: CGFloat {
        get {
            shakes
        } set {
            shakes = newValue
        }
    }
    
    func body(content: Content) -> some View {
        
    }
}
Note that we are creating shakes and animatableData as CGFloat not Int this is because shake represents the progress of the animation. SwiftUI runtime sets this value through animatableData, and it can be any value between the initial and the final value.
For shake effect, we will offset provided content in x axis using swift’s sin function.

The Swift sin() function returns trigonometric sine of an angle (angle should be in radians). The returned value will be in the range -1 through 1.
struct Shake: AnimatableModifier {
    var shakes: CGFloat = 0
    
    var animatableData: CGFloat {
        get {
            shakes
        } set {
            shakes = newValue
        }
    }
    
    func body(content: Content) -> some View {
        content
            .offset(x: sin(shakes * .pi * 2) * 5)
    }
}
Let’s create an extension in View for convenience.

extension View {
    func shake(with shakes: CGFloat) -> some View {
        modifier(Shake(shakes: shakes))
    }
}
We are ready to use this new modifier inside a view. We will create a view to take user’s input for username via TextField view. Upon committing the value(hit return key) on TextField we will reset number of shakes to 0.0 and use a Button to check if the username is equal to “DevTechie”, if values don’t match, we will increase the number of shakes to 4 inside withAnimation block.

Interpolate of number of shakes is determined by what type of Animation is used in withAnimation, to animate the change from 0.0 shakes to 4.0 shakes.
struct DevTechieShakeAnimation: View {
    @State private var username: String = ""
    @State private var numberOfShakes = 0.0
    
    var body: some View {
        NavigationStack {
            VStack(alignment: .leading) {
                Text("Enter username")
                TextField("username", text: $username, onCommit: {
                    numberOfShakes = 0.0
                })
                .textFieldStyle(.roundedBorder)
                .shake(with: numberOfShakes)
                Button("Check") {
                    if username != "DevTechie" {
                        withAnimation {
                            numberOfShakes = 4
                        }
                    }
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}
Build and run.

We can apply this modifier in other views as well.

struct DevTechieShakeAnimation: View {
    @State private var numberOfShakes = 0.0
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Hey DevTechie, \nShake me!")
                    .padding()
                    .font(.largeTitle)
                    .multilineTextAlignment(.trailing)
                    .foregroundStyle(.white)
                    .background(.orange.gradient, in: RoundedRectangle(cornerRadius: 10))
                    .shake(with: numberOfShakes)
                
                Button("Shake") {
                    withAnimation(Animation.easeInOut(duration: 2)) {
                        numberOfShakes = 10
                    }
                }
                
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}
With that we have reached the end of this article. Thank you once again for reading. Don’t forget to subscribe our newsletter at https://www.devtechie.com