Using WatchOS2 Navigation in Swift

It’s rare to have a one controller application, even in something as small as the Apple watch. Multiple View Controllers, or Interface Controllers as they are called in WatchKit, need ways to move between controllers. WatchOS2 has a simplified version of the iOS navigation types. In this lesson we’ll explore these three types of navigation that are available for WatchOS: Page, Hierarchical, and Modal. You’ll find them summarized on this chart below

2016-03-04_12-03-25

In this tutorial we’ll show all three programmatically and on the storyboard.

Set Up the Project

Before starting the project, start the watch and phone simulators. If you do not have direct access to them, you might want to read the Before you Begin section in a earlier post.

Open Xcode if not already open. Create a new project and pick the iOS App with Watchkit App Watch project template.

2016-02-03_05-34-40

You’ll get a configuration window like this:

2016-03-02_06-25-53

Name the project WatchNavigationDemo using Swift as the language. Use a Universal device and uncheck Include Notification Scene as shown above. In the Navigator find these files:

2016-03-02_06-29-30

The Root Controller

Click on the Interface.storyboard in the WatchNavigationDemo WatchKit App group. You will get a blank watch scene with a watch controller and a notifications controller. Drag two buttons and a label to the controller. Set the background color to the system Dark Gray Color(#555555) for both buttons. In the attributes inspector, set one button’s alignment to Top Vertical Alignment, and the other button’s alignment to Bottom Vertical Alignment. Set the label’s text to Root and the font to Headline. Set the label’s vertical and horizontal alignment to Center.

2016-03-02_06-37-05

Click the view controller icon. At the top of the attribute inspector, add a Title and Identity of Root

2016-03-02_06-42-12

For each of our controllers we will add and identity and title. Title will set the title on the watch face. The Identifier names this controller programmatically. If you are only using segues, then you do not need it, but it is mandatory if you are programmatically calling your controllers. If you have neither a segue nor identifier you will get a warning error from Xcode that the controller is unreachable. With our controller done, it should look like this:

2016-03-01_06-11-01

The Page Controllers

Drag another interface controller to the storyboard. Change the background to Green(#AACC00). Add a button and a label. Set the vertical alignment for the button to Bottom. Set the background color to the system Dark Gray Color(#555555). Select the controller and change the Title and Identifier to Page 0. Set the label’s text to Page 0. Set the label’s vertical and horizontal alignment to Center.

Your controller should look like this:

2016-03-01_06-12-42

Select the interface controller icon, then press Command-C to copy the controller. Deselect the controller by clicking on the background of the storyboard. Press Command-V to paste a copy of the controller. This pastes the controller directly on top of the original. It looks like the original controller is selected again. Drag this controller and you will find it is a copy.

2016-03-02_07-09-40

Deselect, and press Command-V again. You now have three Page 0 controllers. Change the Identifier and Title of one copy to Page 1 and the other to Page 2. Arrange the controllers like this

2016-03-02_07-16-45

The Modal Controllers

Add two more controllers, which we’ll use as our modal controllers. Make one controller have a Red(#FF0000) background and the other a Blue(#0000FF) background. Set the Title and Identifier of the red modal to RedModal. Set the Title and Identifier of the blue modal to BlueModal.

2016-03-01_06-10-36

Arrange your controllers like this:

2016-03-02_07-32-44

Page Navigation Through Segues

Page navigation is similar to iOS page navigation  with pages sliding horizontally with a swipe. It is much simpler to set up than the iOS equivalent. All the glances on an apple watch are in a page sequence. If you don’t have a watch you can still see a quick example in the watch simulator. You can swipe up from the watch face to get to glances. Swipe left and right to see two glances.

Like most storyboard transitions between controllers we use segues. For page segues, We control-drag from one controller to another controller, then repeat to make the chain of controllers. Control-drag from the black background of Root to the green of Page 0. When you release the button, a Relationship Segue menu appears:

2016-03-01_06-18-24

Select Next Page. A segue appears.

2016-03-02_08-02-00

Control-drag from Page 0 to Page 1, repeating the process. Then do it again from Page 1 to Page 2. We have four pages connected together.

2016-03-02_08-06-51

Build and run. You have four pages to scroll back and forth through.

page navigation

Hierarchy Navigation by Segue

Like navigation controllers in iOS, hierarchy navigation pushes the next controller to view through a selection device, such as a button. As the name implies you can branch in as any direction you want, unlike the linear page navigation. The settings app on the Apple Watch is an example of a hierarchy navigation you can see on both the watch and the simulator.

Delete all the segues on the storyboard. Select the top button on the root controller. Control-drag from the top button to the Page 0 controller. When you release the mouse button, you will get this menu:

2016-03-01_06-13-40

Select Push. A segue appears between the controllers.

2016-03-03_05-31-22

The process is almost the same as the page segues. The difference is you start your drag from a control, usually a button but it may be a group or table. Make a segue between the bottom button of Root and Page 1. Control drag from the bottom button of Root to Page 1. Select Push for the Action Segue type.

Page and Hierarchy are Mutually Exclusive

Control-drag from Page 1 to Page 2 and make a page segue. Your storyboard should look like this.

2016-03-03_05-46-53

Xcode does not put any warnings up, but what we just did is wrong. You cannot combine page and hierarchy segues. You can only use one or the other on the storyboard. With one exception, once you use one type or segue or the other. You are stuck in the project using that type of navigation only. However, nothing tells you this except the documentation. Clean the project with a Shift-Command-K, then Build and Run. Tap the top button, and you get Page 0.

At the top left of the watch is a back indicator which you tap to go back to root. If you try going to page 1 with the bottom button, there is no page indicator on the Page 1 to get us to page 2. Swipes don’t work either. The system will ignore any illegal segues.

Stop the app and delete the page segue. Control-drag from the button on Page 1 to Page 2 and select the Push segue. Clean, Build and Run. You get all three pages.

hierarchy navigation

Adding Modal Controllers

The last type of controller does not care what type is the one before it. Modal controllers can be called by both page and hierarchy controllers. While the two we’ve learned so far transition horizontally, modal controllers transition vertically. In the watch or simulator, swipe up on the watch face and you have a modal to the glances. The glances, as we discussed earlier, happen to be page navigation. Here’s an exception to the mutual exclusion between hierarchy and pages. If you have a hierarchy then segue to a modal, your modal can segue to pages. However it does not work the other way around. From a page, you cannot call a modal and then a hierarchy.

You set a Modal segue the same way as a hierarchical segue: from control to controller. Stop the app. From the Page 0 button, control-drag to the RedModal controller. Select Modal in the Action Segue menu

2016-03-01_06-23-17

You get a segue between the two.

2016-03-03_06-43-33

Control-drag from the Page 2 button to the BlueModal controller. Select a Modal action segue. From the BlueModal, control-drag to the RedModal. Select the Page relationship segue. Your storyboard should look like this:

2016-03-03_06-44-38

Build and run. All the navigation works.

modal navigation

Programmatic Navigation

We’ve covered navigation on the storyboard. While the mutual exclusive part does provides some limits for building apps, there is enough flexibility to set up your entire app in the storyboard. It’s probably best to setup your applications in the story board, as many of the attributes of a controller are not accessible programmatically. However, many developers prefer programmatic navigation to segues. Sometimes the logic of the code dictates a more programmatic approach. There are three methods that call controllers directly, as shown in the chart above. We’ll discuss each as we code the controllers.

Press Command-N to make a new file. Make a new WatchOS Watchkit file named PageIntefaceController, subclassing WKInterfaceController. Save the file being careful to make sure the group is in the extension.

In the file that appears, add the following code.

    var whichModal = "RedModal"
    @IBOutlet var goModalOutlet: WKInterfaceButton!
    
    @IBAction func goModalAction() {
        presentControllerWithName(whichModal, context: nil)
    }

In the action goModalAction, the method presentViewControllerWithName:context: presents the modal controller with the identifier name from the storyboard. We’ve used a string variable whichModal, which we’ll use in a later iteration to switch between the blue and red modals. The second parameter context, sends data to the destination controller. We set this to nil to not use it. We’ll discuss this parameter in mmore detail below.

Select the InterfaceController.swift file. Add the follow code to the InterfaceController class

@IBAction func topButton() {
    pushControllerWithName("Page 0", context: nil)
}
@IBAction func bottomButton() {
    let names = ["Page 2","Page 1","Page 0"]
    presentControllerWithNames(names, contexts: nil)
 }

The bottomButton action, contains the presentControllerWithNames method. This presents the page views. It has similar parameters to presentControllerWithName, with one change: instead of a single value it uses arrays. The array order is the order of the pages displayed. Our array is in the constant names, counting down pages instead of up as we have been doing. The contexts array has the corresponding context to in the names array. We are not using it yet, so we left that nil.

The action topButton has the method for the hierarchical segue pushControllerWithName:context: Like a navigation controller, it pushes the new interface into view. The parameters are identical to its modal brother.

Connect up your outlets and actions. Go back to the story board, and select the Page 0 controller. In the identity inspector, change the class to PageInterfaceController. Repeat for Page 1 and Page 2. Close the right panel inspector and open the assistant editor. Click on the Root Controller in the storyboard. Drag from the topButton action to the upper button. Drag from the bottomButton action to the bottom button.

Select the Page 0 controller. Connect the outlet and action to the button. Do the same for Page 1 and Page 2.

Clean, build and run. From the root menu, click the top button.

2016-03-03_06-05-00

It goes to a hierarchical controller version of Page 0. Tap the button in Page 0 and we get the red modal.

Go back to the root, and tap the bottom button. You get a page controller starting with Page 2 and counts down pages to 0.

2016-03-03_06-05-30

Tap any button on a controller goes to the red modal.

programmatic navigation

We seem to have broken the mutual exclusive nature of the page and hierarchy controllers. Programmatically,  there’s one more wrinkle. The system does not know which navigation you use until your first use of it on the root. In our app, we set that by the button pressed.

Working with Contexts

Contexts pass data to a destination controller. In the modal and Hierarchical controllers it is a single AnyObject? For pages it is an optional array of AnyObject. Close the assistant editor, and go to the InterfaceController.swift file. Change the actions to this:

    @IBAction func topButton() {
      pushControllerWithName("Page 0", context: "BlueModal")
    }
    @IBAction func bottomButton() {
        let names = ["Page 2","Page 1","Page 0"]
        let contexts = ["RedModal","BlueModal","RedModal"]
        presentControllerWithNames(names, contexts: contexts)
    }

In topButton we pass a string as a context. In bottomButton we added a string array contexts that will pass the string RedModal for Page 0 and 2 and a BlueModal for page 1.

Go to the PageInterfaceController code. Change the awakeWithContext method to this:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    if let newContext = context{
        whichModal = newContext as! String
        goModalOutlet.setTitle(whichModal)
    }
}

The awakeWithContext method is the equivalent to viewDidLoad in iOS, except it receives a context. We can use that context to set up the class. First optionally chain the context’s value. Then assign it, downcasting appropriately. I’m setting which modal we will use this way, and also setting the button’s title to tell you where you are going.

Build and run. Select the bottom button and you will see the contexts changing the page controllers.

contexts

What if we wanted to make a more elegant title for the button? A common way of passing more than one parameter in the context is to use a dictionary. Change InterfaceController’s actions to this:

@IBAction func topButton() {
    let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
    pushControllerWithName("Page 0", context: context)
}
@IBAction func bottomButton() {
    let names = ["Page 2","Page 1","Page 0"]
    let contexts:[[String:AnyObject]] = [
        ["Name":"RedModal","Title":"Red"],
        ["Name":"BlueModal","Title":"Blue"],
        ["Name":"RedModal","Title":"Red"]
    ]
    presentControllerWithNames(names, contexts: contexts)
}

Using a dictionary of type [String:AnyObject], I pass two strings to the array. I could use a [String:String], but I want to demonstrate the more generic case where you could use more than one type of data in the dictionary. Go to PageInterfaceController. Change awakeWithContext like this:

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        if let newContext = context{
            let contextDictionary = newContext as! [String:AnyObject]
            whichModal = contextDictionary["Name"] as! String
            let title = contextDictionary["Title"] as! String
            goModalOutlet.setTitle(title)
        }
    }

We changed the code to read a dictionary instead of a single value. You could do the same with a class or struct as well, though dictionaries are nicely temporary in comparison. Build and run. Click the top button and you find the Blue titled button

2016-03-04_07-07-28

Go Back to Root, then click the bottom button, we have a Red button

2016-03-04_07-07-27

Contexts with Segues

Watchkit also has a equivalent to prepareForSegue for passing data when you use a storyboard segue. Like prepareForSegue, you override the method. For modals and hierarchy there is the method For pages there is the to accommodate the array of contexts. Go to the storyboard and add a push segue between Root and Page 0

2016-03-03_05-31-22

In the attributes inspector, set the Identifier for the segue to Page0. Go to InterfaceController. Comment out the code in topButton

@IBAction func topButton() {
    //let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
    //pushControllerWithName("Page 0", context: context)       
}

Add the following function to your code

override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
    
if segueIdentifier == "Page0"{
    let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
        return context
    } else {
        return nil
    }        
}

The contextForSegueWithIdentifier works similar to prepareForSegue. Take  segueIdentifier and do what you need to set up the destination controller for that specific segue. The difference is we return the context, instead of setting a properties of the destination controller directly. For pages, we do the same thing using arrays and contextsForSegueWithIdentifier

Clean, build and run. Tap the top button on Root You should see no difference between the last run and this one.

2016-03-04_07-07-28

That is most of what you need to know about navigation. There are two or three more thing you might want to know such as dismissals and delegates. We’ll cover those next time as we delve into using tables in WatchOS2.

The Whole Code

InterfaceController.swift

//
//  InterfaceController.swift
//  WatchNavigationDemo WatchKit Extension
//
//  Created by Steven Lipton on 3/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {
    
    @IBAction func topButton() {
        // code commented out for using segues and context
        // code for hierachy (push) segues
        //let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
        //pushControllerWithName("Page 0", context: context)
        
    }
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        // the prepareForsegue of WatchOS for modals and push
        // use contextsForSegueWithIdentifier for pages
        if segueIdentifier == "Page0"{
        let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
            return context
        } else {
            return nil
        }
        
    }
    @IBAction func bottomButton() {
        // A Page controller sequence with a dictionary as a context.
        let names = ["Page 2","Page 1","Page 0"]
        let contexts:[[String:AnyObject]] = [
            ["Name":"RedModal","Title":"Red"],
            ["Name":"BlueModal","Title":"Blue"],
            ["Name":"RedModal","Title":"Red"]
        ]
        presentControllerWithNames(names, contexts: contexts)
    }
   
}

PageInterfaceController.swift

//
//  PageInterfaceController.swift
//  WatchNavigationDemo
//
//  Created by Steven Lipton on 3/3/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class PageInterfaceController: WKInterfaceController {
    var whichModal = "RedModal"
    @IBOutlet var goModalOutlet: WKInterfaceButton!
    
    @IBAction func goModalAction() {
        //present a modal controller
        presentControllerWithName(whichModal, context: nil)
    }
    override func awakeWithContext(context: AnyObject?) {
        // the viewDidLoad of WatchKit/WatchOS. 
        // has a parameter which has the context sent to the controller 
        // So you can set the controller up 
        // we use a dictionary  of [String:AnyObject]
        super.awakeWithContext(context)
        if let newContext = context{
            let contextDictionary = newContext as! [String:AnyObject]
            whichModal = contextDictionary["Name"] as! String
            let title = contextDictionary["Title"] as! String
            goModalOutlet.setTitle(title)
        }
    }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s