SwiftUI: Animation

Mar 29 2022 · Swift 5.5, iOS 15, Xcode 13

Part 1: Beginning with SwiftUI Animation

06. Spinner

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 05. Challenge: Rotation Next episode: 07. Multiple Stages

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 06. Spinner

Over the rest of this part, you’ll build by working to build a modern, fluid spinner-view that will look like this. The beginnings of that spinner, however, are rather humble.

In the starter project, there’s a single ellipse shape that we’ll call a “leaf”.Have a look at the starter code of SpinnerView and observe that: Leaf is a nested view struct and it draws itself by using a view type called Capsule.

Capsule, along with Circle, RoundedRectangle, and …just Rectangle, allow you to easily draw shapes, or use them as clipping masks.

The SpinnerView body is currently very simple. It includes a single leaf shape and, once added on screen, calls its animate(_) method — which is currently empty.

The code is all set for you to jump in and add some flair.

Your first task is to draw the static spinner on screen. Doing this will give you some insight into how to compose shape views and hopefully give you ideas about how to design your own shape animations in the future.

First, command-click on the Leaf instance, and Repeat it.

      ZStack {
        ForEach(/*@START_MENU_TOKEN@*/0 ..< 5/*@END_MENU_TOKEN@*/) { index in
          Leaf()
        }
      }

Make sure to rename item to index, because that’s a more accurate name for it. Creates as many leaves as the leavesCount constant.

ForEach(0..<leavesCount)

That code will draw twelve identical leaves on screen. In fact, all of them are so perfectly identical, that when drawn over each other, the results looks exactly the same as that initial single leaf rendering.

No worries; a few more tweaks and the spinner view will start taking shape. First of all, in order to see more than a single leaf drawn on screen, let’s rotate each one slightly from the previous.

First, add a property for the Leaf type which will help us set the rotation of the capsule shape.

  struct Leaf: View {
    let rotation: Angle

    var body: some View {

You can use degrees this time, if you’d like:

        ForEach(0..<leavesCount) { item in
          Leaf(
            rotation: .init(degrees: <#T##Double#>)
          )
        }

You can change the index and leavesCount to doubles, using the “init” shorthand, because Angles are based on Doubles.

            rotation: .init(degrees: .init(index) / .init(leavesCount))

That will give you evenly-spaced fractional values of a circle. Now just multiply the resulting circle portions by a full circle.

 .init(leavesCount) * 360)

We don’t see any yet, though, since we haven’t used the rotation yet. The place to do that is in the body of Leaf, using a rotationEffect modifier.

        .frame(width: 20, height: 50)
        .rotationEffect(rotation)
    }

Now, there are definitely multiple leaves. But they’re drawn over each other. To make them more distinguishable, offset them from their original center, right before rotating.

        .offset(x: 0, y: 70)
        .rotationEffect(rotation)
    }

Great! We’ve got a nice flower-like layout, that will serve as a good base to create a beautiful animation.

To get that going, you’ll use a timer so you can repeatedly make changes to your state over time. Each time it fires, you’ll animate a different leaf, creating a wave-like effect that goes round and round.

In your SpinnerView, add a new state property, with a default “current index” of negative 1.

  let leavesCount = 12
  @State var currentIndex = -1
  
  var body: some View {

You’ll be updating this continuously from your timer’s callback. Move down to the empty animate() method and start the animation code by creating a new timer, “with time interval” of .15 seconds.

  
  func animate() {
    Timer.scheduledTimer(withTimeInterval: 0.15, repeats: <#T##Bool#>, block: <#T##(Timer) -> Void#>)
  }

Make sure it repeats, and use trailing closure syntax.

    Timer.scheduledTimer(withTimeInterval: 0.15, repeats: true) { timer in

    }

Now, it’s time to set the current leaf index from within the closure. Increment currentIndex, and set it back to 0 when you reach the total number of leaves.

    Timer.scheduledTimer(withTimeInterval: 0.15, repeats: true) { timer in
      currentIndex = (currentIndex + 1) % leavesCount
    }

This will allow you to iterate over each of the leaves repeatedly.

Next, you need to provide that index to the Leaf view so it can render itself differently depending on whether it’s the one to currently animate. Add an isCurrent property to Leaf:

    let rotation: Angle
    let isCurrent: Bool

    var body: some View {

A Leaf will be “current” if the ForEach index is the SpinnerView’s currentIndex.

            rotation: .init(degrees: .init(index) / .init(leavesCount) * 360),
            isCurrent: index == currentIndex
          )

Now, you can adjust the stroke modifier on the Capsule to draw the leaf as white, instead of gray, when it should animate.

.stroke(isCurrent ? Color.white : .gray, lineWidth: 8)

Live Preview, to see the start of an animation.

The current leaf “goes around” and keeps animating indefinitely. However, like with our first color change effort in this course, this is not a “real”, “SwiftUI animation”. It’s just being driven by your timer.

Since the state changes occur pretty quickly (between 6 and 7 times per second) they look sort-of animated, but you can do better!

Let’s invite SwiftUI to the party. Since you’d like to animate the changes to each separate leaf, add an animation modifier to the Leaf body.

        .rotationEffect(rotation)
        .animation(.easeIn(duration: <#T##Double#>)), value: isCurrent)
    }

Let’s go with half a second.

.animation(.easeInOut(duration: 🟩 0.5), value: isCurrent)

That way, we’re intentionally using a duration longer than the timer interval. With that, at least a few leaves will be animating on screen at the same time. Unlike before, now you can see a very fluid and clean crossfade effect on each animating leaf:

That’s not a bad spinner at all, really. But let’s keep iterating over the current result, and make this spinner view truly stunning. First, adjust the duration of the animation to 1.5 seconds like so:

.animation(.easeIn(duration: 🟩 1.5), value: isCurrent)

Now, we’ve got a more subtle, calming fade.It will be more appropriate as we add more effects — we’ll keep it so that no one single animation will dominate the composite animation.

Next, use different offsets based on the isCurrent property.

        .frame(width: 20, height: 50)
        .offset(
          isCurrent
            ? .init(width: 10, height: 0)
            : .init(width: 40, height: 70)
        )
        .rotationEffect(rotation)

And now, the current leaf animates slightly towards the spinner center. It’s kind of like the leaves are floating in water, like a sea anemone’s tentacles.

To finish up this episode, let’s scale the leaves as they move as well.

            : .init(width: 40, height: 70)
        )
        .scaleEffect(isCurrent ? 0.5 : 1)
        .rotationEffect(rotation)

Now, there’s a little bit more “flow”, to it. Next, you’ll learn how to add multiple separate stages to your animation.