Mastering SwiftUI Animations: Keyframe Animations

Author Image

Mazen Kourouche

Jun 13, 2023

Keyframe Animations Blog Cover

Learn how to build keyframe animations in SwiftUI in less than 5 minutes. It's now super easy to add stunning animations to your iOS apps in minutes.


Creating our Animation

We want to build a ‘bounce’ animation that makes it look like our Checkmark Icon is bouncing in the view. To achieve this we want to simulate what would happen in a real scenario. We’d want our icon to squash down, preparing for the bounce, then as it returns to its normal size, it will spring up. We will first set up our view then dive into the animation properties.

This is going to be a simple View with the Image that we are going to animate.

struct KeyframeAnimationView: View { var totalDuration = 1.0 var body: some View { Image(systemName: "checkmark.circle.fill") .resizable() .foregroundStyle(.blue) .frame(width: 100, height: 100) } }

We also have a totalDuration variable which we will use to control the duration of our animation. Currently, it's set to 1.0 (seconds).

Introducing the Animation Properties

To control the different parts of the animations, we need to create a set of animation properties. We’ll define this as a struct so we can provide default values for each property. We’ll add a vertical stretch property to control the vertical size of the view, and a translation property to control its vertical position.

struct AnimationProperties { var translation = 0.0 var verticalStretch = 1.0 }

The Keyframe Animator

Now we will add the keyframeAnimator modifier to our Image. We’ll add the desired modifiers to the content inside the animator’s closure to control the animation.

.keyframeAnimator( initialValue: AnimationProperties(), repeating: true) { content, value in content .scaleEffect( .init(width: 1, height: value.verticalStretch), anchor: .bottom) .offset(y: value.translation) }

The keyframeAnimator is a SwiftUI modifier that creates a keyframe animation for the view. It takes an initialValue argument that defines the initial animation properties. We pass AnimationProperties() here to set the initial translation to 0.0 and the initial verticalStretch to 1.0.

The repeating: true argument makes the animation repeat indefinitely.

The keyframeAnimator also takes a closure that SwiftUI will call for each frame of the animation. SwiftUI passes the current view (content) and the current animation properties (value) to this closure. We modify the content by applying a scaleEffect and an offset.

The scaleEffect changes the size of the image. We set the width factor to 1 and the height factor to value.verticalStretch. This means that the width of the image remains the same while its height changes based on verticalStretch.

The anchor: .bottom argument makes the bottom of the image stay in the same place during the stretch, so we can achieve a more realistic looking animation

The offset moves the image vertically. The y: value.translation argument sets the vertical position of the image based on translation.

Defining the Keyframes

Keyframes are a fundamental concept in animations, allowing us to specify the state of an object at a particular point in time, and have the system animate the transitions between these points.

A KeyframeTrack in SwiftUI is a series of keyframes that define the value of a certain property at different points in time. Each KeyframeTrack corresponds to a single property that we want to animate.

We have two properties to animate: verticalStretch and translation. So, we'll define two KeyframeTracks.

Within a KeyframeTrack, we use Keyframe instances to define the value of the property at different points in time. There are different types of keyframes, each representing a different way of transitioning to the next keyframe:

  • CubicKeyframe: This type of keyframe represents a smooth transition to the next keyframe, following a cubic Bezier curve. This gives us a nice, fluid animation.
  • SpringKeyframe: This type of keyframe represents a transition that mimics the physics of a spring, providing a bouncy effect.
  • MoveKeyframe: This type of keyframe immediately moves to the given value without interpolating.
  • LinearKeyframe: This type of keyframe uses simple linear interpolation.

Let’s add our keyframes to our animation:

keyframes: { _ in KeyframeTrack(\.verticalStretch) { SpringKeyframe(0.6, duration: 0.2 * totalDuration) CubicKeyframe(1, duration: 0.2 * totalDuration) CubicKeyframe(1.2, duration: 0.6 * totalDuration) CubicKeyframe(1.1, duration: 0.25 * totalDuration) SpringKeyframe(1, duration: 0.25 * totalDuration) } KeyframeTrack(\.translation) { CubicKeyframe(0, duration: 0.2 * totalDuration) CubicKeyframe(-100, duration: 0.4 * totalDuration) CubicKeyframe(-100, duration: 0.4 * totalDuration) CubicKeyframe(0, duration: 0.5 * totalDuration) } }

Getting the keyframes can take some time but don’t be scared to play around with the values to fine-tune your animation to perfection

It's also crucial to remember that the durations of all keyframes in a KeyframeTrack should add up to the totalDuration we defined earlier. This ensures a smooth transition from one keyframe to another over the specified total duration.

If we put this all together, our view code looks like this:

struct KeyframeAnimationView: View { var totalDuration = 1.0 var body: some View { Image(systemName: "checkmark.circle.fill") .resizable() .foregroundStyle(.blue) .frame(width: 100, height: 100) .keyframeAnimator(initialValue: AnimationProperties(), repeating: true) { content, value in content .scaleEffect(.init(width: 1, height: value.verticalStretch), anchor: .bottom) .offset(y: value.translation) } keyframes: { _ in KeyframeTrack(\.verticalStretch) { SpringKeyframe(0.6, duration: 0.2 * totalDuration) CubicKeyframe(1, duration: 0.2 * totalDuration) CubicKeyframe(1.2, duration: 0.6 * totalDuration) CubicKeyframe(1.1, duration: 0.25 * totalDuration) SpringKeyframe(1, duration: 0.25 * totalDuration) } KeyframeTrack(\.translation) { CubicKeyframe(0, duration: 0.2 * totalDuration) CubicKeyframe(-100, duration: 0.4 * totalDuration) CubicKeyframe(-100, duration: 0.4 * totalDuration) CubicKeyframe(0, duration: 0.5 * totalDuration) } } } }

By providing the totalDuration variable, we can now easily modify the duration of the animation without having to play around with the keyframe values.


Conclusion

Keyframe animations in SwiftUI open a whole new dimension in creating interactive and dynamic UI designs. With the power of KeyframeTrack and different keyframes like SpringKeyframe and CubicKeyframe, you can create animations that are visually appealing and highly precise.

Don't shy away from playing around with keyframes - the magic happens when you experiment and explore.

The journey of animation in SwiftUI is as exciting as it is limitless. Stay tuned, keep coding, and most importantly, enjoy the process!