Getting Started with SwiftUI Animations

In this tutorial, you’ll learn how to add fancy animations with SwiftUI. You’ll go from basic animations to complex and custom spring animations. By Michael Katz.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Tinkering With Timing

If you’re having trouble seeing the subtle differences, you can slow down the animation by making it take more time. Replace the animation modifier with:

.animation(.easeIn(duration: 5))

Specifying a longer duration will make the timing curve more noticeable.

An advantage of building and running in the simulator instead of the SwiftUI preview window is that you can enable the Debug ▸ Slow Animations flag. This will drastically slow down any animation so you can see the subtle differences more clearly. This way, you don’t have to add extra duration parameters.

You can use a few other levers to control the timing of an animation, besides the timing curve and duration. First, there’s speed.

Create a new property at the top of ContentView:

let moonAnimation = Animation.easeInOut.speed(0.1)

This constant just stores the animation so you can use it more easily in the code later. It also gives you a single spot to change things around.

Next, replace the Image animation modifier:

.animation(moonAnimation)

Now, the moon will resize very slowly. Try changing the speed to 2 and the animation will be quite zippy.

In addition to speed, you can also add delay. Change moonAnimation to:

let moonAnimation = Animation.easeInOut.delay(1)

Now, the animation has a one-second delay. Tapping the row flashes in the moon list, then later, the button will change size. Delay is most useful when animating multiple properties or objects at once, as you’ll see later.

Finally, you can use modifiers to repeat an animation. Change the animation to:

let moonAnimation = Animation.easeInOut.repeatForever(autoreverses: true)

The button will now pulse forever. You can also use repeatCount(autoreverses:) to repeat the animation a limited number of times.

When you’re done experimenting, set the animation back to:

let moonAnimation = Animation.default

Simultaneous Animations

.animation is a modifier that stacks on to a SwiftUI View like any other. If a view has multiple changing attributes, a single Animation can apply to all of them.

Add the following rotation effect by placing it between the Image and scaleEffect lines:

.rotationEffect(.degrees(self.toggleMoons(planet.name) ? -50 : 0))

This adds a little rotation to the moon button so the crescent lines up sideways when the moon view appears. These will animate together since you add the animation at the end.

Rotated moon button

Of course, you can specify separate animations for each attribute. For example, add the following modifier after the rotationEffect modifier:

.animation(.easeOut(duration: 1))

This gives the rotation a one-second animation, so you’ll notice the rotation ending slightly later compared to the scaling effect. Next, change moonAnimation to:

let moonAnimation = Animation.default.delay(1)

This delays the size animation by one second, so the scale starts after the rotation finishes. Try it out.

Finally, you can choose not to animate a particular attribute change by specifying nil for animation. Change the rotationEffect animation to:

.animation(nil)

And change the moonAnimation back to:

let moonAnimation = Animation.default

Now, only the size animates.

Animating State Changes

You’ve spent a while perfecting the animation of the moon button, but what about that big view that drops in with all the circles?

Well, you can animate any state transition with a simple withAnimation block. Replace the content of the button’s action block with:

withAnimation {
  self.showMoon = self.toggleMoons(planet.name) ? nil : planet.name
}

withAnimation explicitly tells SwiftUI what to animate. In this case, it animates the toggling of showMoon and any views that have attributes that depend on it.

If you watch the animation now, you’ll see the moons view fade in using the default appear animation and the list row slides out of the way to make room for it.

You can also supply a specific animation to the explicit animation block. Replace withAnimation with:

withAnimation(.easeIn(duration: 2))

This now uses an eased-in animation with a two-second duration instead of the default.

If you try this out, the slowed animation probably doesn’t look right. There are better ways to animate views coming in and out of the hierarchy.

Before moving on, change the withAnimation to:

withAnimation(.easeInOut)

Transitions

A transition covers how a view is inserted or removed. To see how it works, add this modifier to the MoonList in the if self.toggleMoons(planet.name) block:

.transition(.slide)

Now, instead of fading in, the view will slide in. Slide animates in from the leading edge and out through the trailing.

Transitions are of the AnyTransition type. SwiftUI comes with a few pre-made transitions:

  • .slide: You’ve already seen this one in action — it slides the view in from the side.
  • .opacity: This transition fades the view in and out.
  • .scale: This animates by enlarging or shrinking the view.
  • .move: This is like slide, except that you can specify the edge.
  • .offset: Moves the view in an arbitrary direction.

Go ahead and try some of these transitions to get a sense of how they work.

Combining Transitions

You can also combine transitions to compose your very own custom effects. At the top of ContentView.swift, add this extension:

extension AnyTransition {
  static var customTransition: AnyTransition {
    let transition = AnyTransition.move(edge: .top)
      .combined(with: .scale(scale: 0.2, anchor: .topTrailing))
      .combined(with: .opacity)
    return transition
  }
}

This combines three transitions: a move from the top edge, a scale from 20% anchored to the top trailing corner and an opacity fade effect.

To use it, change the transition line at the MoonList instance to:

.transition(.customTransition)

Build and run your project and try opening a moon list. The combined effect is as if the view swoops in and out from the moon button.

Moon button animation on click

Asynchronous Transitions

If you want, you can also make your entrance transition different from your exit transition.

In ContentView.swift, replace the definition of customTransition with:

static var customTransition: AnyTransition {
  let insertion = AnyTransition.move(edge: .top)
    .combined(with: .scale(scale: 0.2, anchor: .topTrailing))
    .combined(with: .opacity)
  let removal = AnyTransition.move(edge: .top)
  return .asymmetric(insertion: insertion, removal: removal)
}

This keeps the swoop in insertion, but now the moon view exits the screen by moving to the top.

Springs

Right now, the moons list just shows the moons stacked on top of each other in concentric circles. It would look a lot nicer if they were spaced out, and maybe even animated in.

In MoonView.swift, add the following modifier to the end of the body chain:

.onAppear {
  withAnimation {
    self.angle = self.targetAngle
  }
}

This causes a random angle to be set on the orange ball representing the moon. The moon will then animate from zero degrees on the circle to the new location, in a straight line.

Simple animation of the moons

This animation isn’t awesome yet, so you’ll need to add a little bit more pizazz. Spring animations allow you to add a little bounce and jiggle to make your views feel alive.

In SolarSystem.swift add this method to SolarSystem:

func animation(index: Double) -> Animation {
  return Animation.spring(dampingFraction: 0.5)
}

This helper method creates a spring animation.

Then, in makeSystem(_:), in the last ForEach, append the following modifier to the self.moon line:

.animation(self.animation(index: Double(index)))

This adds the animation to each of the moon circles. Don’t worry about the index right now; you’ll use it in the next section.

Next time you load the view, there will be a little bounce as the moons take their final positions.