SwiftUI Field Guide

Animation Curves

In the real world, things move. On our screens, this is not really possible; we can only simulate movement. For example, if we draw a red box and move it one pixel to the right on every frame, it looks like the box is moving to the right. Timing curves help us control this movement.

If we always move at a constant speed, this is a linear animation using a linear timing curve. Here's what that looks like:

Code
struct CurveExample: View {    @State private var isTrailing = false        let animation = Animation.linear        var body: some View {        Color.red            .frame(width: 48, height: 48)            .frame(maxWidth: .infinity,                   alignment: isTrailing                       ? .trailing                       : .leading)            .contentShape(.rect)            .onTapGesture {                isTrailing.toggle()            }            .animation(animation,                       value: isTrailing)            .padding(20)    }}
Preview
Waiting to start Swift runtime…
Waiting for page JavaScript · 0.0s elapsed
Curve

To most of us, this looks a little uncanny. Objects in the real world move in a different way: typically, the velocity increases (by applying force) rather than the position. We can simulate this by using an easing curve. For example, the easeInOut timing curve starts slow, picks up speed, and slows down again at the end:

Code
struct CurveExample: View {    @State private var isTrailing = false        let animation = Animation.easeInOut        var body: some View {        Color.red            .frame(width: 48, height: 48)            .frame(maxWidth: .infinity,                   alignment: isTrailing                       ? .trailing                       : .leading)            .contentShape(.rect)            .onTapGesture {                isTrailing.toggle()            }            .animation(animation,                       value: isTrailing)            .padding(20)    }}
Preview
Waiting to start Swift runtime…
Waiting for page JavaScript · 0.0s elapsed
Curve
Ease InEase OutEase In Out

The type for expressing timing curves in SwiftUI is Animation. Above, we have seen cubic timing curves. These can all be expressed as unit curves, which we'll look at a bit further down.

Before iOS 17, the default timing curve was easeInOut with a duration of 0.35 seconds. For almost all built-in curves, we can specify a duration.

Springs

Spring curves are the new default as of iOS 17. At first, we might think of springs as something that bounces. However, for animations, the biggest advantage is that springs are a model of the physical world and take velocity into account. Similar to UIKit Dynamics, they are a physical simulation of a motion system. The biggest benefit is that we can retarget a spring animation while keeping the velocity. We'll say more about this in interruptible animations.

Springs can arrive at their target value and still oscillate for a bit without us really noticing it visually. The animation is said to be logically complete. This is something that shows up in the API as well, and when we add completion handlers, we can work with this property as well.

Code
struct CurveExample: View {    @State private var isTrailing = false        let animation = Animation.spring        var body: some View {        Color.red            .frame(width: 48, height: 48)            .frame(maxWidth: .infinity,                   alignment: isTrailing                       ? .trailing                       : .leading)            .contentShape(.rect)            .onTapGesture {                isTrailing.toggle()            }            .animation(animation,                       value: isTrailing)            .padding(20)    }}
Preview
Waiting to start Swift runtime…
Waiting for page JavaScript · 0.0s elapsed
Curve
SpringSmoothSnappy

SwiftUI also has a .bouncy spring. Depending on the use case, this can be especially nice when moving objects on the screen, as it slightly overshoots.

Code
struct CurveExample: View {    @State private var isTrailing = false        let animation = Animation.bouncy        var body: some View {        Color.red            .frame(width: 48, height: 48)            .frame(maxWidth: .infinity,                   alignment: isTrailing                       ? .trailing                       : .leading)            .contentShape(.rect)            .onTapGesture {                isTrailing.toggle()            }            .animation(animation,                       value: isTrailing)            .padding(20)    }}
Preview
Waiting to start Swift runtime…
Waiting for page JavaScript · 0.0s elapsed
Curve

Note: Overshooting animations are not always a great choice. For example, when we're animating a color or opacity, overshooting might have unexpected effects. Animations based on curves might be a better fit there.

We can also configure the physics simulation behind spring animation ourselves by providing mass, damping and stiffness. Play around with the parameters to see what they do:

Code
struct CurveExample: View {    @State private var isTrailing = false        let animation =        Animation.spring(Spring(mass: 1,                                stiffness: 100,                                damping: 10))        var body: some View {        Color.red            .frame(width: 48, height: 48)            .frame(maxWidth: .infinity,                   alignment: isTrailing                       ? .trailing                       : .leading)            .contentShape(.rect)            .onTapGesture {                isTrailing.toggle()            }            .animation(animation,                       value: isTrailing)            .padding(20)    }}
Preview
Waiting to start Swift runtime…
Waiting for page JavaScript · 0.0s elapsed
Curve
Mass
1
Stiffness
100
Damping
10

There are also convenience initializers on Spring that lets us specify duration and bounciness. These are used to then compute mass, damping and stiffness.

Note: There is also the older .interpolatingSpring static constructor, which has been available since iOS 13. This is a different type of spring animation that does not start from the existing velocity when retargeting. Instead, it blends the two animations.

Modifying Curves

We can modify animations by adding a delay, repeating them a fixed number of times or indefinitely, or by changing their speed.

Unit Curves

We can create custom timing curves as well. There is a static constructor on Animation that lets us specify a UnitCurve. Unit curves are cubic Bézier curves with a normalized coordinate space with values between 0 and 1. They can also overshoot by having values outside of that range.

In the interactive example below, we can play around with the control points of the unit curve and see what the animation looks like:

Curve

Drag either control point. Use the arrow keys for precise adjustments.

Preview
Start
End
Duration
1

Custom Curves

It's also possible to create completely custom curves using the CustomAnimation protocol. This gives us full control of the interpolation. We can also choose the merge behavior and specify what the velocity is at any given time.