Chapter 20
The Menu: Pull It Together
We've got all the components, let's get this beast put together now.
Chapter 20 Contents
We’re now going to work in the Menu.swift
file, and our plan is this:
- add all the layers to this class’ canvas
- attach a gesture to this class’ canvas
- add methods for revealing and hiding the menu’s layers
- add sounds that play when the menu opens and closes
- add a little instruction label to help the user
Add the Layers
Create the following class variables:
var menuRings : MenuRings!
var menuIcons : MenuIcons!
var menuSelector : MenuSelector!
var menuShadow : MenuShadow!
Then, modify setup()
to look like this:
override func setup() {
//clear the background
canvas.backgroundColor = clear
//make the canvas frame fairly small
canvas.frame = C4Rect(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 the main WorkSpace
to look like:
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:
self.
to
self.menuSelector.
Your method should now look like:
func createGesture() {
canvas.addLongPressGestureRecognizer { (location, state) -> () in
switch state {
case .Changed:
self.menuSelector.update(location)
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:
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
}
}
menuIsVisible
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:
var menuIsVisible = false
At the end of revealMenu
add:
delay(1.0) {
self.menuVisible = 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:
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 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:
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:
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:
if self.shouldRevert {
self.hideMenu()
self.shouldRevert = false
}
Once the menu has fully revealed itself it will check to see if it should revert.
Finally, add the following case to the top of the switch statement:
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:
self.canvas.interactionEnabled = true
And add the following to the .Cancelled
state of the switch statement in the gesture block:
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:
func createGesture() {
//add a long press gesture to the menu's canvas
canvas.addLongPressGestureRecognizer { (location, state) -> () in
switch state {
case .Began:
self.revealMenu()
case .Changed:
self.menuSelector.update(location)
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:
let hideMenuSound = C4AudioPlayer("menuClose.mp3")!
let revealMenuSound = C4AudioPlayer("menuOpen.mp3")!
In setup()
, tune the sounds like this:
hideMenuSound.volume =
Then, at the top of revealMenu()
call:
revealMenuSound.play()
And, at the top of hideMenu()
call:
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 to be told 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:
var instructionLabel : UILabel!
Note we’re using
UILabel
because we want to have 2 lines of text that are center-aligned, andC4TextShape
doesn’t have these functionalities.
Add the following method to create the instruction:
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:
var timer : C4Timer!
Then, create two methods to reveal and hide the instruction:
func showInstruction() {
C4ViewAnimation(duration: 2.5) {
self.instructionLabel?.alpha = 1.0
}.animate()
}
func hideInstruction() {
C4ViewAnimation(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 at the end of setup()
like so:
timer = C4Timer(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:
if instructionLabel?.alpha > 0.0 {
hideInstruction()
}
And, add this to the top of revealMenu()
:
timer.stop()
Run it, and wait.
Hoooo-ahhh.
The Shadow
Simple.
At the top of revealMenu()
add the following line:
menuShadow.reveal?.animate()
Then, in the delay(0.66)
of hideMenu()
add the following line:
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.