Chapter 21
The Menu – Pull it Together

Estimated Time


We’re now going to work in the Menu.swift file, and our plan is this:

  1. add all the layers to this class’ canvas
  2. attach a gesture to this class’ canvas
  3. add methods for revealing and hiding the menu’s layers
  4. add sounds that play when the menu opens and closes
  5. add a little instruction label to help the user

Add the Layers

Create the following class variables:

1
2
3
4
5
var menuRings : MenuRings!
var menuIcons : MenuIcons!
var menuSelector : MenuSelector!
var menuShadow : MenuShadow!
var shouldRevert = false

Then, modify setup() 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
22
23
24
25
override func setup() {
    //clear the background
    canvas.backgroundColor = clear
    //make the canvas frame fairly small
    canvas.frame = Rect(0,0,80,80)

    //create the rings
    menuRings = MenuRings()
    
    //create the selector
    menuSelector = MenuSelector()
    
    //create the icons
    menuIcons = MenuIcons()

    //create the shadow
    menuShadow = MenuShadow()
    menuShadow.canvas.center = canvas.bounds.center
    
    //add the canvases of each object in specific order (back to front)
    canvas.add(menuShadow.canvas)
    canvas.add(menuRings.canvas)
    canvas.add(menuSelector.canvas)
    canvas.add(menuIcons.canvas)
}

Then, modify setup() in your project’s WorkSpace to look like:

1
2
3
4
5
6
override func setup() {
    canvas.backgroundColor = COSMOSbkgd
    let menu = Menu()
    menu.canvas.center = canvas.center
    canvas.add(menu.canvas)
}

If you run the app now you should see this:

It’s subtle, but you can actually see that the layers are there… Both the rings and the icons are visible.

Add the Gesture

Now, go back to MenuSelector.swift, cut the entire createGesture() method and add it to this class.

You’ll see Xcode is going to complain about this:

To fix these, all you need to do is change:

1
self.

to

1
self.menuSelector.

Your method should now look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func createGesture() {
    canvas.addLongPressGestureRecognizer { (locations, center, state) -> () in
        switch state {
        case .Changed:
            self.menuSelector.update(center)
        case .Cancelled, .Ended, .Failed:
            self.menuSelector.currentSelection = -1
            self.menuSelector.highlight.hidden = true
            self.menuSelector.menuLabel.hidden = true
        default:
            _ = ""
        }
    }
}

The gesture is now a part of the menu’s canvas and not anymore part of the selector.

Reveal the Menu

Now, we want the menu to open and close with all the animations in proper order and timed nicely. To do this, add the following two methods to your class:

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
30
31
32
33
34
func revealMenu() {
    menuShadow.reveal?.animate()
    menuRings.thickRingOut?.animate()
    menuRings.thinRingsOut?.animate()
    menuIcons.signIconsOut?.animate()
    
    delay(0.33) {
        self.menuRings.revealHideDividingLines(1.0)
        self.menuIcons.revealSignIcons?.animate()
    }
    delay(0.66) {
        self.menuRings.revealDashedRings?.animate()
        self.menuSelector.revealInfoButton?.animate()
    }
}

func hideMenu() {
    menuRings.hideDashedRings?.animate()
    menuSelector.hideInfoButton?.animate()
    menuRings.revealHideDividingLines(0.0)
    
    delay(0.16) {
        self.menuIcons.hideSignIcons?.animate()
    }
    delay(0.57) {
        self.menuRings.thinRingsIn?.animate()
    }
    delay(0.66) {
        self.menuIcons.signIconsIn?.animate()
        self.menuRings.thickRingIn?.animate()
        self.menuShadow.hide?.animate()
        self.canvas.interactionEnabled = true
    }
}

Most of the interaction requires the menu to be open, so we’ll create a variable that we can check as needed. Add the following to your class:

1
var menuIsVisible = false

At the end of revealMenu() add:

1
2
3
delay(1.0) {
   self.menuIsVisible = true
}

We know that the animations take a total of 1 second to complete when revealing the menu, so we delay until this point to set the menu as visible.

At the top of hideMenu() add:

1
self.menuIsVisible = false

… because we know that as soon as the hide method starts executing, the menu is not fully visible anymore.

You should also add the same line to the top of revealMenu(), because it forces the state to be correct when you reveal.

Your reveal/hide methods should look like:

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
30
31
32
33
34
35
36
37
38
39
func revealMenu() {
    menuIsVisible = false
    menuShadow.reveal?.animate()
    menuRings.thickRingOut?.animate()
    menuRings.thinRingsOut?.animate()
    menuIcons.signIconsOut?.animate()
    
    delay(0.33) {
        self.menuRings.revealHideDividingLines(1.0)
        self.menuIcons.revealSignIcons?.animate()
    }
    delay(0.66) {
        self.menuRings.revealDashedRings?.animate()
        self.menuSelector.revealInfoButton?.animate()
    }
    delay(1.0) {
        self.menuIsVisible = true
    }
}

func hideMenu() {
    self.menuIsVisible = false
    menuRings.hideDashedRings?.animate()
    menuSelector.hideInfoButton?.animate()
    menuRings.revealHideDividingLines(0.0)
    
    delay(0.16) {
        self.menuIcons.hideSignIcons?.animate()
    }
    delay(0.57) {
        self.menuRings.thinRingsIn?.animate()
    }
    delay(0.66) {
        self.menuIcons.signIconsIn?.animate()
        self.menuRings.thickRingIn?.animate()
        self.menuShadow.hide?.animate()
        self.canvas.interactionEnabled = true
    }
}

Behavioural Logic

We’ll add the first bit of logic to the gesture. Under the .Cancelled state in the switch statement, and after the first if, add the following:

1
2
3
4
5
if self.menuIsVisible {
    self.hideMenu()
} else {
    self.shouldRevert = true
}

If the gesture ends, is canceled, or fails then this logic will check two things. If the menu is visible then it will revert, otherwise it flags shouldRevert so that when the menu finishes opening it will know to close automatically.

Then, in the delay(1.0) block of revealMenu(), add the following:

1
2
3
4
if self.shouldRevert {
    self.hideMenu()
    self.shouldRevert = false
}

Once the menu has fully revealed itself it will check to see if it should revert.

Add the following case to the top of the switch statement:

1
2
case .Began:
   self.revealMenu()

When the gesture begins (after a default 0.25s of being pressed) it will open the menu.

Finally, in hideMenu() add the following to the delay(0.66) block:

1
self.canvas.interactionEnabled = true

And add the following to the .Cancelled state of the switch statement in the gesture block:

1
self.canvas.interactionEnabled = false

This prevents the user from being able to interact with the canvas while the menu is reverting to the closed state. And, sets the interaction to true when we know the menu has fully closed.

Your createGesture() should now look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func createGesture() {
    //add a long press gesture to the menu's canvas
    canvas.addLongPressGestureRecognizer { (locations, center, state) -> () in
        switch state {
        case .Began:
            self.revealMenu()
        case .Changed:
            self.menuSelector.update(center)
        case .Cancelled, .Ended, .Failed:
            self.menuSelector.currentSelection = -1
            self.menuSelector.highlight.hidden = true
            self.menuSelector.menuLabel.hidden = true
            if self.menuIsVisible {
                self.hideMenu()
            } else {
                self.shouldRevert = true
            }
            self.canvas.interactionEnabled = false
        default:
            _ = ""
        }
    }
}

Now, make damn sure you’re calling this from the Menu class’ setup:

HAWT.

Sounds

Add the reveal and hide sounds to the menu. Create the following two class variables:

1
2
let hideMenuSound = AudioPlayer("menuClose.mp3")!
let revealMenuSound = AudioPlayer("menuOpen.mp3")!

In setup(), tune the sounds like this:

1
2
hideMenuSound.volume = 0.64
revealMenuSound.volume = 0.64

Then, at the top of revealMenu() call:

1
revealMenuSound.play()

And, at the top of hideMenu() call:

1
hideMenuSound.play()

Run it. Listen. It’s so lovely.

Hidden Instruction

Let’s add an instruction label that tells the user to press and hold on the menu.

We found that people needed a prompt to press and hold on the menu before they could figure out how to use the app. So, we added this label to help them. We also assumed that after reading the instruction for the first time they wouldn’t have to read it again, so we decided to prevent it from reappearing after the first time it disappears.

Create the following class-level variable:

1
var instructionLabel : UILabel!

Note we’re using UILabel because we want to have 2 lines of text that are center-aligned, and TextShape doesn’t have these functionalities.

Add the following method to create the instruction:

1
2
3
4
5
6
7
8
9
10
11
12
func createInstructionLabel() {
    instructionLabel = UILabel(frame: CGRect(x: 0,y: 0,width: 320, height: 44))
    instructionLabel.text = "press and hold to open menu\nthen drag to choose a sign"
    instructionLabel.font = UIFont(name: "Menlo-Regular", size: 13)
    instructionLabel.textAlignment = .Center
    instructionLabel.textColor = .whiteColor()
    instructionLabel.userInteractionEnabled = false
    instructionLabel.center = CGPointMake(view.center.x,view.center.y - 128)
    instructionLabel.numberOfLines = 2
    instructionLabel.alpha = 0.0
    canvas.add(instructionLabel)
}

Next create a timer that will control when the label will appear:

1
var timer : Timer!

Then, create two methods to reveal and hide the instruction:

1
2
3
4
5
6
7
8
9
10
11
func showInstruction() {
    ViewAnimation(duration: 2.5) {
        self.instructionLabel?.alpha = 1.0
        }.animate()
}

func hideInstruction() {
    ViewAnimation(duration: 0.25) {
        self.instructionLabel?.alpha = 0.0
        }.animate()
}

In the show() method notice that we stop the timer. We do this because we only want the timer to fire once. You might be thinking: “then why not trigger the reveal from a delay?”… Because we want to be able to stop the timer before it first fires if the user has already opened the menu… and we can’t stop a delay

Next, create the timer and call createInstructionLabel() at the end of setup() like so:

1
2
3
4
5
6
createInstructionLabel()

timer = Timer(interval: 5.0) {
    self.showInstruction()
}
timer?.start()

This sets the reveal to happen 5 seconds after the main canvas has set up, which is definitely enough time for someone who knows how to use the app to press on the menu and short enough that it appears in time for a user who doesn’t know how to use the menu.

Now, add a call to hideInstruction() to execute at the top of the revealMenu() method, with a bit of logic to run it only if the instruction label is visible:

1
2
3
if instructionLabel?.alpha > 0.0 {
    hideInstruction()
}

And, add this to the top of revealMenu():

1
timer.stop()

Run it, and wait.

Hoooo-ahhh.

The Shadow

Simple.

At the top of revealMenu() add the following line:

1
menuShadow.reveal?.animate()

Then, in the delay(0.66) of hideMenu() add the following line:

1
self.menuShadow.hide?.animate()

Now the background gets dark when the menu opens.

Fin.

That’s “it” the menu is 98% complete. The next chapter will go through pulling everything together, including adding controls for the menu to interact with the info panel and the parallax background.

Here’s a copy of Menu.swift

Take a break.

Have a beer.