Chapters

Hide chapters

SwiftUI Animations by Tutorials

First Edition · iOS 16 · Swift 5.7 · Xcode 14

Section I: SwiftUI Animations by Tutorials

Section 1: 11 chapters
Show chapters Hide chapters

1. Introducing SwiftUI Animations
Written by Bill Morefield

Small touches can help your app stand out from the competition in the crowded App Store. Animations provide one of these small delightful details.

Used correctly, animations show an attention to detail that your users will appreciate and a unique style they’ll remember. Using animations purposefully provides your users subtle and practical feedback as your app’s state changes.

Up until the release of SwiftUI, creating animations was quite a tedious task, even for the simplest of animations. Luckily, SwiftUI is often clever enough to automatically animate your state changes, or provide you with more granular control when the default animations don’t cut it.

First, you’ll explore the basic native animations included in SwiftUI resulting from state changes, the transformation of a value that a view depends on. You’ll then explore view transitions, a type of animation that SwiftUI applies to views when inserted or removed from the screen. These animations provide a base of knowledge you’ll use throughout this book.

Creating Animations

To begin, download and extract the materials for this chapter. Open the starter folder, which contains the starter project for this chapter. Then, open AnimationCompare.xcodeproj to start working on this chapter.

Run the project by selecting Product ▸ Run or press Cmd-R. When the project starts in the simulator, you’ll see two tabs:

The first tab contains the user interface for an app that helps a developer explore different types of animations and manipulate various animation parameters to see their effects. The user can add multiple animations and run them in tandem.

The second tab contains a red square you can show or hide using a button. You’ll use this tab to explore view transitions later in this chapter.

Exploring the Starter App

Inside the Models folder, open AnimationData.swift. You’ll find the AnimationData struct, which holds the properties used by the different types of built-in animations.

Open AnimationCompareView.swift and look for the Add Animation button. When the user taps it, the app creates a new struct with a set of default values and adds it to the animations array. The user can change these values, but none of the animations work yet. You’ll fix that now.

First, look for the location state property:

@State var location = 0.0

As the state changes, you’ll use this property to animate views in your app. First, you need to provide a way for the user to change this state. Immediately inside the VStack, add the following code:

// 1
Button("Animate!") {
  // 2
  location = location == 0 ? 1 : 0
}
.font(.title)
.disabled(animations.isEmpty)

The code above:

  1. Creates a button that changes the state property location to animate views when tapped.

  2. Toggles the value of location between 0.0 and 1.0. You’ll use this later to animate the views on screen.

Notice that you disable the button when the animations array is empty. This prevents users from tapping the button before creating any animations.

Adding Your First Animation

Open AnimationView.swift. Near the top of the view, look for this line:

@Binding var location: Double

This property contains the location passed in from the parent view. When the value changes in AnimationCompareView, it also changes inside this view, since it’s a Binding. Inside each AnimationView, SwiftUI will notice the state change and trigger two animations that you specify.

Currently, AnimationView contains a Text view wrapped inside a GeometryReader. Replace this Text view with:

HStack {
  // 1
  Image(systemName: "gear.circle")
    .rotationEffect(.degrees(360 * location))
  Image(systemName: "star.fill")
    // 2
    .offset(x: proxy.size.width * location * 0.8)
}
.font(.title)
// 3
.animation(
  // 4
  .linear(duration: animation.length),
  // 5
  value: location
)

Here’s what each part of the new view does:

  1. You place two images in an HStack. You apply a rotation effect to the first image that multiplies the location property by 360 degrees. Since location will vary between zero and one, the result will toggle between zero and 360 degrees. The key is that a change in location changes the view’s state.

  2. The second image has an offset applied that multiples the width of the view, taken from the GeometryProxy, by the location property and multiples that by 0.8. As a result, when location is zero, the offset is zero, and when location is one, the offset is 80% of the width of the view. Since SwiftUI applies the offset to the view’s leading edge, multiplying by 0.8 keeps the view from floating off the screen.

  3. There are several ways to tell SwiftUI you want to animate a state change. Here you use animation(_:value:) on the HStack. This method creates the most straightforward SwiftUI animation, an implicit animation. You apply it to the HStack so that both views included within have the animation applied to them. Sounds simple? That’s the beauty of animations in SwiftUI!

  4. The first parameter to animation(_:value:) defines the type of animation, which is a linear animation in this case. You then pass the duration parameter, telling SwiftUI the animation should take the amount of time specified in animation.length to complete. Most animations should last between 0.25 and 1.0 seconds as these values allow the user time to notice the animation without feeling too long and intrusive.

  5. When you apply an implicit animation, you specify the value whose change will trigger the animation. Explicitly setting the state change lets you use different animations with different state changes.

Run the app and add an animation. Next, tap Animate!. The gear icon makes one revolution, and the star slides to the right side of the view.

Linear Animation
Linear Animation

If you tap Animate! again, you’ll see the gear spin in the opposite direction while the star slides back to the left. Think for a moment about why the opposite movement takes place. Here’s a hint: remember what the Animate! button does.

Since the Animate! button returns the property to its original value of zero, the animation reverses. The rotationEffect(_:anchor:) method interprets greater values as clockwise rotation. Therefore, the initial change from zero to one turns into a degree change from zero to 360. This change animates as a set of increasing clockwise rotations. The change back to zero causes counterclockwise animation as the value decreases.

Linear animations work best for views that pass through but do not start or end within the scene. In the real world, a car passing by a window would look routine while moving at a constant speed, but a vehicle instantly achieving full speed from a stop would seem odd. Our minds expect something that starts or stops within our view to accelerate or decelerate.

In the next section, you’ll explore eased animations, another type of animation that matches this behavior.

Creating Eased Animations

Instead of linear movement, eased animations provide acceleration or deceleration at one or both endpoints. The types of eased animations differ by where the change in speed applies.

The most common is the ease out animation. It starts faster than a linear animation before decelerating toward the end. Ease out animations are often the best choice in a user interface since that fast initial motion gives the feeling your app is quickly responding to the user. Here’s the graph of the movement against time:

Ease Out

An ease in animation reverses these steps. It starts more slowly than a linear animation before accelerating. If you were to graph the movement against time, it would look like this:

Ease In

The next eased animation combines the previous two. Ease in-out animations accelerate, as in the ease in animation, before decelerating, as in the ease out animation. For ease in and ease in-out animations, you usually want to keep the duration less than 0.5 seconds so it feels more responsive to your user.

The movement graphed against time looks like a combination of the other two graphs:

Ease In Out

Applying Eased Animations

The app already lets the user select eased animations, so next, you’ll add support for those. Open AnimationView.swift and add the following new computed property after the location property:

var currentAnimation: Animation {
  switch animation.type {
  case .easeIn:
    return Animation.easeIn(duration: animation.length)
  case .easeOut:
    return Animation.easeOut(duration: animation.length)
  case .easeInOut:
    return Animation.easeInOut(duration: animation.length)
  default:
    return Animation.linear(duration: animation.length)
  }
}

This computed property converts the AnimationType enum of animation to the matching SwiftUI animation using a duration from the length property.

Next, change the animation(_:value:) modifier to:

.animation(
  currentAnimation,
  value: location
)

This code sets the animation using the new computed property you just added, so it has the animation specified in the animation struct. Any animation you haven’t implemented will fall back to a linear animation.

Run the app and create two animations. Tap the second and change the type to Ease In-Out.

Changing animation to ease in out.
Changing animation to ease in out.

Tap Back and then Animate! to see the difference between the two animations.

Comparing eased and linear animations
Comparing eased and linear animations

You can slow down animations inside the Simulator using the Debug ▸ Slow Animations toggle to better view the sometimes subtle differences between animations.

Notice the ease in out animation moves slower at first before passing the linear animation and then slowing down at the end. Since you specified the same duration for both animations, they take the same time to complete.

Add two more animations and set one to Ease In and the other to Ease Out. Change the length of one animation and rerun it to see how they compare. Notice the shape of the animation doesn’t change. Only the time it takes the animation to complete changes.

Comparing multiple animations
Comparing multiple animations

While the linear and eased animations let you set the animation’s duration, SwiftUI also provides general modifiers you can apply to any animation.

In the next section, you’ll learn two common modifiers that help you customize the duration of your animation.

Modifying Animations

By default, an animation starts immediately when the state changes, but since your app lets the user specify a delay for each animation, you’ll add support for it. Open AnimationView.swift and replace the current animation(_:value:) modifier with:

.animation(
  currentAnimation
    .delay(animation.delay),
  value: location
)

You add the delay(_:) modifier to the animation and specify the delay in seconds. Run the app and add two animations. Edit the second animation and set the delay to 0.5 seconds. Tap Back and tap the Animate! button to see the effect.

While the first animation begins when you tap the button, the second animation doesn’t start until 0.5 seconds later. A delay doesn’t affect the duration or movement of the animation. However, it can provide a sense of flow or order between multiple animations tied to a single state change.

Changing Animation Speed

Another useful modifier lets you change the animation’s speed independent of type and parameters. You add the speed(_:) modifier and pass a ratio of the base speed to the desired speed.

A value lower than one will result in a slower animation, while a value greater than one will speed up the animation.

You’ll use this to implement a slowdown effect that will make it easier for the user to notice the differences between animations, similar to the Simulator menu option.

Open AnimationCompareView.swift. Add the following new property after the existing ones:

@State var slowMotion = false

You’ll use this boolean property to indicate when the user wants to slow the animations. Next, add the following toggle control to the view as the first item inside the List, before the ForEach:

Toggle("Slow Animations (¼ speed)", isOn: $slowMotion)

Now the user can use this toggle to specify when they want to slow down the animations. Next, open AnimationView.swift and add the following property.

var slowMotion = false

The parent view can now indicate when to slow down the animations on this view, defaulting to false.

Next, replace the existing animation modifier with:

.animation(
  currentAnimation
    .delay(animation.delay)
    .speed(slowMotion ? 0.25 : 1.0),
  value: location
)

Recall that values lower than one passed to the speed(_:) modifier cause the animation to slow down. Passing 0.25 will cause the animation to take four times as long (1 / 0.25 = 4) as it otherwise would have.

Go back to AnimationCompareView.swift. To pass the new property to the view, add the new slowMotion parameter to your AnimationView:

AnimationView(
  animation: animation,
  location: $location,
  slowMotion: slowMotion
)

Run the app, add an animation and tap the Animate! button. You’ll see the familiar one-second linear animation. Now toggle on Slow Animations and tap Animate! again.

Using speed(_:) to slow animations
Using speed(_:) to slow animations

Your animation now takes four seconds, or four times longer, to complete. To verify all animations run slower, add another animation and change its length to 0.5 seconds.

When you animate them, you’ll see the second animation takes two seconds (4 x 0.5 seconds) or half as long as the first animation.

Now that you’ve seen some of the modifiers you can apply to animations, you’ll look at the last type of animation: spring animation.

Springing Into Animations

Spring animations are popular because they seem more natural. They usually end with a slight overshoot and add some “bounce” at their end. The animation values come from the model of a spring attached to a weight.

Imagine a weight attached to one end of a spring. Attach the other end to a fixed point and let the spring drop vertically with the weight at the bottom. The weight will bounce several times before coming to a full stop.

The slowdown and stop come from friction acting on the system. The reduction creates a damped system. Graphing the motion over time produces a result like this:

8 16 24 x 32
Dampened simple harmonic motion

There are two types of spring animations. The interpolatingSpring(mass:stiffness:damping:initialVelocity:) animation uses this damped spring model to produce values. This animation type preserves velocity across overlapping animations by adding the effects of each animation together.

To experiment with this, open AnimationView.swift and add the following new case to the currentAnimation computed property, before the default case:

case .interpolatingSpring:
  return Animation.interpolatingSpring(
    // 1
    mass: animation.mass,
    // 2
    stiffness: animation.stiffness,
    // 3
    damping: animation.damping,
    // 4
    initialVelocity: animation.initialVelocity
  )

Each of the parameters maps to an element of the physical model. Here’s what they do:

  1. mass reflects the mass of the weight.

  2. stiffness defines how resistant the spring is to being stretched or compressed.

  3. damping maps to gravity and friction that slows down and stops the motion.

  4. initalVelocity reflects the weight’s velocity when the animation starts.

Notice that these parameters aren’t correlated with the linear and eased animations you used earlier. You also don’t have a direct way to set the animation length as you did before.

Run the app and add two animations.

Change the type for the second one to an Interpolating Spring and keep the default values, which include the default mass and initialVelocity if you don’t specify them to the method.

Default interpolating spring animation.
Default interpolating spring animation.

Go back to the main screen and tap Animate!, and you’ll see a much different animation. The star and gear move past the end point before bouncing slightly backward. The movement will repeat with the motion decreasing until it stops.

Even with the extra movement, the spring completes faster than the one-second linear animation.

Note: Before moving on, try to experiment with the different animation parameters and get a grasp for how each of them effects the animation.

Increasing the mass causes the animation to last longer and bounce further on each side of the end point. A smaller mass stops faster and moves less past the end points on each bounce.

Increasing the stiffness causes each bounce to move further past the end points but with a smaller effect on the animation’s length.

Increasing the damping slows the animation faster. If you set an initial velocity, it changes the initial movement of the animation.

Another Way to Spring

SwiftUI provides a second spring animation method you can apply using the spring(response:dampingFraction:blendDuration:) method. The underlying model doesn’t change, but this method abstracts the four different physics-based arguments with two simpler arguments.

Open AnimationView.swift and add the following case before the default case:

case .spring:
  return Animation.spring(
    response: animation.response,
    dampingFraction: animation.dampingFraction
  )

The spring’s response and dampingFraction internally map to the appropriate physics-based values of interpolatingSpring.

The response parameter acts similarly to the mass in the physics-based model. It determines how resistant the animation is to changing speed. A larger value will result in an animation slower to speed up or slow down.

The dampingFraction parameter controls how quickly the animation slows down. A value greater than or equal to one will cause the animation to settle without the bounce that most associate with spring animations.

A value between zero and one will create an animation that shoots past the final position and bounces a few times, similarly to the previous section.

A value near one will slow faster than a value near zero. A value of zero won’t settle and will oscillate forever, or at least until your user gets frustrated and closes your app.

Note that you aren’t using the blendDuration parameter of spring(response:dampingFraction:blendDuration:) as that only applies when combining multiple animations, which is more advanced than you’ll examine in this chapter.

Run the app and add two animations. Change the type of the first to Interpolating Spring and the type of the second to Spring. Tap Animate!, and you’ll notice that the animations are similar despite the different parameters.

Change the first animation to Spring and experiment by changing the values to see the effect of mass and stiffness on the animation. Slowing the animation down will help with the often subtle differences between spring animations.

Comparing spring animations.
Comparing spring animations.

You now understand the basics of SwiftUI animations and can use the app to explore and fine-tune animations in your apps.

Next, you’ll look at one final category of animations: view transitions.

Using View Transitions

View transitions are a subset of animations that animate how views appear or vanish. You’ll often use them when a view only appears when your app is in specific states. Open TransitionCompareView.swift. You’ll see a simple view consisting of a button that toggles the boolean showSquare property. When showSquare is true, it shows a red square with rounded corners.

Run the app, go to the Transitions tab and tap the button a few times to see it in action.

Note that there’s no animation when the square appears and vanishes. That’s because transitions only occur when you apply an animation to the state change. Earlier in the chapter, you used implicit animations where the animation(_:value:) modifier implied SwiftUI should animate the view. Now, you will explicitly tell SwiftUI to create an animation when showSquare changes. To do so, go to the action for the button and change it to:

withAnimation {
  showSquare.toggle()
}

With the animation applied, you can apply a transition using the transition(_:) modifier on a view. Look for the conditional statement to show the rectangle and add the following modifier after foregroundColor():

.transition(.scale)

Run the app and repeat the steps. You’ll see the square shrink to a single point.

You can also use this explicit withAnimation(_:_:) function on the animations you used earlier in this chapter. However, it can only specify a single animation and will apply that animation to all changes resulting from the code within the function.

The scale transition you applied here makes the view appear to originate or vanish by scaling to a provided ratio of the view’s size. By default, the view scales down to zero at a point in its center. You can change either of these values.

Change the transition for the rounded square to:

.transition(.scale(scale: 2.0, anchor: .topLeading))

Run the app. When you hide the view, the square will expand to twice its original size, with the scaling centered around the top leading corner of the view before vanishing.

Note: When running in the simulator, the vanishing of the scale animation might end abruptly due to a bug in SwiftUI. If this happens to you, try running the code on a device, or a different Simulator.

Additional Transitions

You can specify the default fade transition using the opacity transition. Change the transition to read:

.transition(.opacity)

Run the app. You’ll see the view now vanishes and appears with a fade-in/out animation.

Another transition is offset(x:y:), which lets you specify that the view should offset from its current position. Change the transition to read:

.transition(.offset(x: 10, y: 40))

Run the app. The view slides slightly to the right and down before being removed. When it returns, it appears at the same position it vanished from before returning to the original location.

You can also specify the view should move towards a specified edge using the move(edge:) transition. Change the current transition to:

.transition(.move(edge: .trailing))

Run the app. Now the view slides off toward the trailing edge when you tap the button to hide it. When you show the square, the view will appear from the same edge it moved toward before vanishing.

Having a view transition by appearing on the leading edge and vanishing toward the trailing edge is common enough that SwiftUI includes a predefined specifier: the slide transition. Change the transition to:

.transition(.slide)

Run the app, and you’ll see the view acts similar to the move(edge:) transition it replaced, except the view now appears from the leading edge and vanishes toward the trailing edge. This transition provides different animations for inserting and removing the view.

Head over to the next section to learn how you too can create these custom asynchronous transitions!

Using Asynchronous Transitions

You can specify different transitions when the view appears and vanishes using the asymmetric(insertion:removal:) method on AnyTransition.

Add the following code after the showSquare state property:

// 1
var squareTransition: AnyTransition {
  // 2
  let insertTransition = AnyTransition.move(edge: .leading)
  let removeTransition = AnyTransition.scale
  // 3
  return AnyTransition.asymmetric(
    insertion: insertTransition,
    removal: removeTransition
  )
}

Here’s how your new transition works:

  1. You specify the transition as a computed property on the view. Doing so helps keep the view code less cluttered and makes it easier to change in the future.
  2. Next, you create two transitions. The first is a move(edge:) transition and the second is a scale transition.
  3. You use the .asymmetric(insertion:removal:) transition and specify the insertion and removal transition for your view.

Finally, change the transition to read:

.transition(squareTransition)

This method tells SwiftUI to apply the transition from the squareTransition property to the view.

Run the app. When you tap the button, you’ll see the view does as you’d expect. The view appears from the leading edge and scales down when removed.

You’ve now explored the basics of animations and view transitions in SwiftUI. In the remainder of this book, you’ll delve deeper into more complex animations.

Challenge

Modify the transitions view for this chapter’s app to let the user specify a single transition or separate insert and removal transitions. For each type of transition, let the user select the additional values supported by the transition. Apply these transitions to the square when the user taps the button.

To help you get started, you’ll find data structures that can hold the properties for transitions in TransitionData.swift. You’ll also find a view letting the user specify these properties in TransitionTypeView.swift. Check the challenge project in the materials for this chapter for a possible solution.

Key Points

  • SwiftUI animations are driven by state changes. The change of a value that affects a view.
  • View transitions are animations applied to views when SwiftUI inserts or removes them.
  • Linear animations represent a constant-paced animation between two values.
  • Eased animations apply acceleration, deceleration or both to the animation.
  • Spring animations use a physics-based model of a spring.
  • You can delay or change the speed of animations.
  • Most animations should last between 0.25 and 1.0 seconds in length. Shorter animations often aren’t noticeable, while longer animations risk annoying your user who just wants to get something done.
  • View transitions can animate by opacity, scale or movement. You can use different transitions for the insertion and removal of views.

Where to Go From Here?

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.