Chapter 17
Animating the Menu Rings

Estimated Time


Even though I’ve separated this chapter from the previous one, we’re still going to be working in MenuRings.swift. Run the app again and make sure that you see this:

If you don’t, then go back to the previous chapter and make sure all the variables are set for the inner state of each line / ring component of the menu.

Thick Ring Animations

For each component we’re going to create a specific animation object for the out and in transitions. Starting with the thick ring, add the following two variables to your class:

1
2
var thickRingOut : ViewAnimation?
var thickRingIn : ViewAnimation?

Now, create a method that sets them up:

1
2
3
4
5
6
7
8
9
10
11
func createThickRingAnimations() {
    thickRingOut = ViewAnimation(duration: 0.5) {
        self.thickRing?.frame = self.thickRingFrames[1]
    }
    thickRingOut?.curve = .EaseOut
    
    thickRingIn = ViewAnimation(duration: 0.5) {
        self.thickRing?.frame = self.thickRingFrames[0]
    }
    thickRingIn?.curve = .EaseOut
}

This method sets up two animations, the first “out” sets the new frame of the thick ring and tells the shape to update its path which forces the shape to redraw its path to fit the frame. The second is simply the opposite.

Let’s test it out.

I create these two methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func animOut() {
    delay(1.0) {
        self.thickRingOut?.animate()
    }
    delay(2.0) {
        self.animIn()
    }
}

func animIn() {
    delay(1.0) {
        self.thickRingIn?.animate()
    }
    delay(2.0) {
        self.animOut()
    }
}

Then in setup I run:

1
2
self.createThickRingAnimations()
animOut()

And I get this:

Thin Ring Animations (OUT)

Let’s start by building the out animations for our thin rings. This step differs a bit from the previous because we have to coordinate 5 lines instead of one, so we use a loop to build them and an array to store them.

Add the following variable to your class:

1
var thinRingsOut : ViewAnimationSequence?

Then, add this method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func createThinRingsOutAnimations() {
    var animationArray = [ViewAnimation]()
    for i in 0..<self.thinRings.count-1 {
        let anim = ViewAnimation(duration: 0.075 + Double(i) * 0.01) {
            let circle = self.thinRings[i]
            //for each animation greatre than the first
            if (i > 0) {
                //animate the opacity of the ring to 1.0
                ViewAnimation(duration: 0.0375) {
                    circle.opacity = 1.0
                    }.animate()
            }
            
            circle.frame = self.thinRingFrames[i+1]
        }
        anim.curve = .EaseOut
        animationArray.append(anim)
    }
    thinRingsOut = ViewAnimationSequence(animations: animationArray)
}

This creates an animation sequence variable, and then populates that by setting up animations based on the target frames we set waaaaay back in the previous chapter.

The method creates an animation array, then runs a loop that:

  1. Grabs each of the thin rings from smallest to largest
  2. Creates an animation whose duration is based on the current index (0.075+Double(i) + 0.01)
  3. Checks if the index of each lines is more than 0, i.e. not the inner ring
  4. Creates an opacity animation for each of the non-inner circles that fades them in and executes this immediately
  5. Grabs the current target frame which is +1 from the current index.
  6. Sets the frame of the current ring and calls updatePath
  7. Sets the animation curve of the main animation .EaseOut
  8. Adds the animation to the array

When the loop completes it constructs the thinRingsOut sequence.

Thin Rings Animation (IN)

Animating back in is pretty much the same process as out, but with only a few simple changes.

Add the following variable to your class:

1
var thinRingsIn : ViewAnimationSequence?

Then add this method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func createThinRingsInAnimations() {
    var animationArray = [ViewAnimation]()
    for i in 1...self.thinRings.count {
        let anim = ViewAnimation(duration: 0.075 + Double(i) * 0.01, animations: { () -> Void in
            let circle = self.thinRings[self.thinRings.count - i]
            if self.thinRings.count - i > 0 {
                ViewAnimation(duration: 0.0375) {
                    circle.opacity = 0.0
                    }.animate()
            }
            circle.frame = self.thinRingFrames[self.thinRings.count - i]
        })
        anim.curve = .EaseOut
        animationArray.append(anim)
    }
    thinRingsIn = ViewAnimationSequence(animations: animationArray)
}

Here are the differences:

  1. This bit of math means the loop works backwards from the outer ring to the inner: self.thinRings.count - i
  2. Animates the opacities to 0.0
  3. Since we always want to grab the “previous” number for our arrays (e.g. -1) this is why the loop executes like this: for i in 1...self.thinRings.count

Dashed Ring Animations

To finish these animations off, create two new animation variables:

1
2
var revealDashedRings : ViewAnimation?
var hideDashedRings : ViewAnimation?

Add the following method:

1
2
3
4
5
6
7
8
9
10
11
12
13
func createDashedRingAnimations() {
    revealDashedRings = ViewAnimation(duration: 0.25) {
        self.dashedRings[0].lineWidth = 4
        self.dashedRings[1].lineWidth = 12
    }
    revealDashedRings?.curve = .EaseOut
    
    hideDashedRings = ViewAnimation(duration: 0.25) {
        self.dashedRings[0].lineWidth = 0
        self.dashedRings[1].lineWidth = 0
    }
    hideDashedRings?.curve = .EaseOut
}

Check It.

To see the animate out / in, modify the animOut and animIn methods to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func animOut() {
    delay(1.0) {
        self.thickRingOut?.animate()
        self.thinRingsOut?.animate()
        self.revealDashedRings?.animate()
    }
    delay(2.0) {
        self.animIn()
    }
}

func animIn() {
    delay(1.0) {
        self.thickRingIn?.animate()
        self.thinRingsIn?.animate()
        self.hideDashedRings?.animate()
    }
    delay(2.0) {
        self.animOut()
    }
}

Then, add the following to setup() before calling animOut():

1
2
3
createThinRingsOutAnimations()
createThinRingsInAnimations()
createDashedRingAnimations()

Run it and you should see this:

By now you may have noticed that the timing is strange for the rings and dashes. Don’t worry about that now, we’ll deal with timing when we pull everything together at the end.

Nearly there.

Dividing Lines

Animating the dividing lines is simple, except for the fact that Jake’s concept is for them to randomly animate the order in which they appear. Translation: every time the lines animate in or out the sequence should be different than the previous time.

This is how I solved the issue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func revealHideDividingLines(target: Double) {
    var indices = [0,1,2,3,4,5,6,7,8,9,10,11]
    
    for i in 0...11 {
        delay(0.05*Double(i)) {
            let randomIndex = random(below: indices.count)
            let index = indices[randomIndex]
            
            ViewAnimation(duration: 0.1) {
                self.menuDividingLines[index].strokeEnd = target
            }.animate()
            
            indices.removeAtIndex(randomIndex)
        }
    }
}

Add that to your class.

This method does the following:

  1. Takes a target (should be 0.0, or 1.0)
  2. Creates an array of indexes
  3. Runs a loop that executes 12 times
  4. Each iteration grabs a random entry from the indices array
  5. Then, it animates the strokeEnd of the dividing line for the retrieved index to the target value
  6. After executing the animation it removes the retrieved index from the indices array.

Then, try it out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func animOut() {
    delay(1.0) {
        self.thickRingOut?.animate()
        self.thinRingsOut?.animate()
    }

    delay(1.5) {
        self.revealHideDividingLines(1.0)
    }

    delay(2.5) {
        self.animIn()
    }

}

func animIn() {
    delay(0.25) {
        self.revealHideDividingLines(0.0)
    }

    delay(1.0) {
        self.thickRingIn?.animate()
        self.thinRingsIn?.animate()
    }
    delay(2.0) {
        self.animOut()
    }
}

Which gives me the following:

Cleaning House

Now, scrap the call to self.animOut() and delete the two animation methods.

Next, create two new methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
func createRingsLines() {
    createThickRing()
    createThinRings()
    createDashedRings()
    createMenuDividingLines()
}

func createRingsLinesAnimations() {
    createThickRingAnimations()
    createThinRingsOutAnimations()
    createThinRingsInAnimations()
    createDashedRingAnimations()
}

And then update your setup to look like this:

1
2
3
4
5
6
public override func setup() {
  canvas.backgroundColor = clear
  canvas.frame = Rect(0,0,80,80)
  createRingsLines()
  createRingsLinesAnimations()
}

Finally, add one more variable to the class:

1
var menuIsVisible = false

This last variable will be used in a later chapter.

Grab a copy of MenuRings.swift