Chapter 16
Menu Rings
It's time to build the backbone of our radial menu.
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.
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:
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:
(0.075+Double(i) + 0.01)
+1
from the current index.updatePath
.EaseOut
When the loop completes it constructs the thinRingsOut
sequence.
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:
self.thinRings.count - i
0.0
-1
) this is why the loop executes like this: for i in 1...self.thinRings.count
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
}
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.
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:
0.0
, or 1.0
)strokeEnd
of the dividing line for the retrieved index to the target
valueThen, 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:
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