Make App Pie

Training for Developers and Artists

Swift Watchkit: Working With Modal Views Part 3: Modal Page Views

Photo Jun 11, 6 33 39 AM

Apple’s  documentation for WatchKit  is quite clear, even when it is lying. The documentation states you can have  hierarchical navigation or page navigation but not both. Here’s is where it lies:  you can have a page-based navigation as part of a hierarchical navigation scheme. There is a special modal view version of page views, which you can use in a hierarchical scheme.  In the conclusion of this lesson on modal views, we’ll look at  using the modal  page view.

In the last part, we learned we can call a modal view programmatically using the method presentControllerWithName(_name:, context:). For example, we had this code for presenting the number controller modally:

@IBAction func numberButtonPressed() {
    //myModel.myNumber = 3.14
    myModel.delegate = self
    //prepare the context
    let context: AnyObject? = myModel
    //present the controller
    presentControllerWithName("Number", context: context)
}

For a pages control, we just make everything in the modal control plural. The method is presentControllerWithNames(_names:, contexts:). For a modal page controller, use arrays instead of a single values for the parameters. In the code from the previous lessons add this under the numberButtonPressed action.

@IBAction func pagesButtonPressed() {
    let pageNames = ["RunPage","WalkPage","PizzaPage"]
    let pageContexts:[AnyObject]? = [myModel.myNumber,myModel.myColorString,myModel]
    presentControllerWithNames(pageNames, contexts: pageContexts)
 }

Line 1 has an array of interface identifiers. The system will present the pages in this order, starting with the Run page, going to the Walk page and finishing with the Pizza Page. Line 3 gives different contexts for the corresponding page. Our Run page for example will take the Float value myNumber from the model. Similarly, the Walk page will take a String. Let’s design a storyboard then write some code to see how this would work.

Adding the Pages Button

Go to the watch app storyboard. On the I am Root interface, add another button, which will snap itself to the bottom. Label it Pages.

2015-06-11_07-32-21

Open the assistant editor. Next to the action pageButtonPressed is the open circle. Drag from the circle to the Pages button to connect the button.

Adding the Three Page Controllers.

Press Command-N and make a new controller subclassing WKInterfaceController named RunPageInterfaceController. Be sure to group it in the WatchKit extension. Do the same to make two more interface controllers named  WalkPageInterfaceController and PizzaPageInterfaceController.

Go back to the storyboard.  Add a new interface to the storyboard. Add a group on the interface.

2015-06-11_06-38-13

Select the group. In the attribute inspector, set the Height  to Relative to Container.  Change the color of the group to Light Gray.  Change the Layout to Vertical.

2015-06-11_06-38-58

Add two labels to the group

2015-06-11_06-40-54

Select the bottom label. Set the Position to Horizontal: Center, Vertical:Center. Change the font to 50 point System Centered.

2015-06-11_06-40-17

Select the controller icon on this interface. Press Command-C to copy the interface.  Click on the white background of the storyboard to deselect everything. Press Command-V to paste a copy of the interface.  The pasted interface is on top of the current interface. Drag the interface and you will now have two.

2015-06-11_06-47-12

Deselect everything again and press Commmand-V. Drag again and you will have three controllers

2015-06-11_06-47-55

In the identity inspector, Make the ID of one Controller RunPage and its name Run. Make the ID of the second controller WalkPage, and the name Walk. Make the ID of the third PizzaPage and the name Pizza.  Using emoji you can find at Control-Command-Space, set up the three interfaces to look like this:

2015-06-11_06-43-54

Go to the identity inspector. Give RunPage a class of RunPageInterfaceController, WalkPage a class of WalkPageInterfaceController, and PizzaPage a class of PizzaPageInterfaceController.

Build and run.  Press the Pages button  and the Run interface shows. Swipe to the left and you get the walk page, another swipe give you the Pizza Page. Tap on the word Pizza on the dark background. The modal dismisses.

Adding Contexts

While you can add delegates to each page. You don’t have to. Modal pages very likely won’t interact, but just show information, like glances. This time, we’ll transmit information through the context and use it differently in each page.

Wire up the  Page controllers.  With the assistant editor open, select the RunPage interface. Control-Drag the smaller label to the  code, just under the class declaration.  Add an outlet statusLabel .  After the outlet, add a property:

var number:Float = 0.0

In the code, add the following to awakeWithContext:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    //Update the status label with the context.
    number = context as! Float
}

The RunPage is the first element of the context  array. We set the context there to the Float value of  myModel.myNumber. We only have to unwrap the Float value to use it. We’ll display our result in willActivate. Change that method to this:

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    let displayString = String(format:"Run %0.2f minutes",number)
    statusLabel.setText(displayString)
}

The code changes the number to a String, then displays the string on the label.

  We’ve finished our first controller.  With the assistant editor open, click on the WalkPage in the storyboard. The WalkPageController should be in the assistant editor. In this controller, we take a String with a color for our context. We’ll instantiate a new instance of the model, then add that string to the model to compute a UIColor for the string. We’ll use that model to change the color of the group’s background and to tell us what color we have.

We need two outlets for this.  Control-drag from the group to the code in the assistant editor. Add an outlet named backgroundGroup. Control-drag from the label to the code. Add an outlet named statusLabel. Finally add an instance of  DemoModel named myModel. You should have just added this code:

@IBOutlet weak var backgroundGroup: WKInterfaceGroup!
@IBOutlet weak var statusLabel: WKInterfaceLabel!
let myModel = DemoModel()

We’ll cast the context’s AnyObject? to type String, then add to the model with the colorFromString method on the model we used in the first lesson on modals. Change awakeWithContext  to the following code:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
   // Configure interface objects here.
   //Context is a String, but we need a string and color
    //so we use a new model.
    let colorString = context as! String
    myModel.colorFromString(colorString)
}

Once again, set up the display in the willActivate code:

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    let displayString = myModel.myColorString + " Walking"
    statusLabel.setText(displayString)
    backgroundGroup.setBackgroundColor(myModel.myColor)
}

Click on the pizza interface to select it and bring up the PizzaPageInterfaceController code in the assistant editor. This time we pass DemoModel as the context. Wire up the controller by first adding the following outlets and actions to the code:

var myModel = DemoModel()
@IBOutlet weak var backgroundGroup: WKInterfaceGroup!
@IBOutlet weak var statusLabel: WKInterfaceLabel!

Drag from the empty circle to the left of the backgroundGroup to a spot on the Pizza interface. Release the button. Do the same between statuslabel and the label.

We have only one line to add to awakeToContext. We’ll need to cast the context to a DemoModel object.

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    myModel = context as! DemoModel
 }

Since we are pizza themed in this page, let’s have a different pizza based on color. Change willActivate to the following:

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        var pizzaType = "Cheese"
        switch myModel.myColorString{
            case "Red": pizzaType = "Pepperoni"
            case "Green": pizzaType = "Veggie"
            case "Blue": pizzaType = "Gorgonzola"
            default: pizzaType = "Cheese"
        }
        let displayString = pizzaType + " Pizza!!"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

We created a variable pizzaType.  Using a switch statement, we mapped the colors to appropriate types of pizzas (Red meat, Green vegetables and Blue cheese), assign the pizza type in pizzaType.  Instead of displaying the color, we display  the pizza.

Build and run. Assuming the simulator behaves itself. you should have your three buttons.

2015-06-11_06-57-58

Set the number  to 3.5 by pressing the number button and using the slider, then pressing Done.

2015-06-11_06-58-34

Set the color to Blue in the Color modal.

2015-06-03_06-13-02

When done your watch looks like this:

2015-06-11_07-01-41

Tap Pages. The Run Page appears:

2015-06-11_06-58-57

You’ll notice our number is in the label. Swipe left to get the walk page and our selected color: .

2015-06-11_07-02-38

Swipe left again to get a pizza based on the blue color:

2015-06-11_07-02-02

Our modal controllers work perfectly. Modals are meant for interrupted use of a more active view. Page modals are best used for viewing data in a glance fashion. Actually glances are a type of modal controller.  Modals are the last of the basic types of controllers in Watchkit. In future lessons we’ll use these with a few very handy complex controllers. In the next lesson, we’ll look at  one of these: Text input when you don’t have room for a keyboard.

The Whole Code

Storyboard

2015-06-15_07-37-17

InterfaceController.swift

//
//  InterfaceController.swift
//  SwiftWatchModalDemo WatchKit Extension
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController,ColorModalDelegate, NumberInterfaceDelegate {
    
    //MARK: Outlets and Properties
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    @IBOutlet weak var colorButton: WKInterfaceButton!
    var myModel = DemoModel()
    
    //MARK: - Actions
    
    @IBAction func numberButtonPressed() {
        //myModel.myNumber = 3.14
        myModel.delegate = self
        //prepare the context
        let context: AnyObject? = myModel
        //present the controller
        presentControllerWithName("Number", context: context)
    }
    
    @IBAction func pagesButtonPressed() {
        let pageNames = ["RunPage","WalkPage","PizzaPage"]
        let pageContexts:[AnyObject]? = [myModel.myNumber,myModel.myColorString,myModel]
        presentControllerWithNames(pageNames, contexts: pageContexts)
    }
  
    //MARK: - Delegates
    func colorDidFinish(context: AnyObject?) {
        myModel = context as! DemoModel
        dismissController()
        updateDisplay()
    }
    
    func numberDidFinish(context:Float){
        myModel.myNumber = context
        updateDisplay()
        dismissController()
    }
    
    //MARK: - Instance Methods
    func updateDisplay(){
        let statusText = String(format: "%1.2f", myModel.myNumber)
        statusLabel.setText(statusText)
        colorButton.setBackgroundColor(myModel.myColor)
    }
    
    //MARK: - Life Cycle
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        if segueIdentifier == "ColorsModal" {
            myModel.delegate = self
            return myModel
        }
        return nil
    }
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        updateDisplay()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

DemoModel.swift

//
//  DemoModel.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class DemoModel:NSObject{
    var myNumber:Float = 0.0
    var myColorString = "Black"
    var myColor = UIColor(white: 0.2, alpha: 0.73)
    var delegate:AnyObject? = nil
    
    func colorFromString(colorString:String){
        myColorString = colorString
        switch colorString {
        case "Red": myColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.73)
        case "Green":myColor = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.73)
        case "Blue":myColor = UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.73)
        default: myColor = UIColor(white: 0.2, alpha: 0.73)
        myColorString = "Black"
        }
    }
}

WalkPageInterfaceController.swift

//
//  WalkPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class WalkPageInterfaceController: WKInterfaceController {

    @IBOutlet weak var backgroundGroup: WKInterfaceGroup!
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    let myModel = DemoModel()
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    
        // Configure interface objects here.
        //Context is a String, but we need a string and color
        //so we use a new model.
        let colorString = context as! String
        myModel.colorFromString(colorString)
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        let displayString = myModel.myColorString + " Walking"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

RunPageInterfaceController.swift

//
//  RunPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class RunPageInterfaceController: WKInterfaceController {

    var number:Float = 0.0
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
        //Update the status Label with the context. 
        number = context as! Float
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        let displayString = String(format:"Run %0.2f minutes",number)
        statusLabel.setText(displayString)
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

PizzaPageInterfaceController.swift

//
//  PizzaPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class PizzaPageInterfaceController: WKInterfaceController {
    var myModel = DemoModel()
    @IBOutlet weak var backgroundGroup: WKInterfaceGroup!
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        var pizzaType = "Cheese"
        switch myModel.myColorString{
            case "Red": pizzaType = "Pepperoni"
            case "Green": pizzaType = "Veggie"
            case "Blue": pizzaType = "Gorgonzola"
            default: pizzaType = "Cheese"
        }
        let displayString = pizzaType + " Pizza!!"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

ColorsModalInterfaceController.swift

//
//  ColorsModalInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol ColorModalDelegate{
    
    func colorDidFinish(context:AnyObject?)
}

class ColorsModalInterfaceController: WKInterfaceController {

    //MARK: Properties
    var myModel = DemoModel()
    var delegate:ColorModalDelegate! = nil
    //MARK: - Action Methods
    @IBAction func redButtonPressed() {
        updateModel("Red")
        
    }
    @IBAction func greenButtonPressed() {
        updateModel("Green")
        
    }
    @IBAction func blueButtonPressed() {
        updateModel("Blue")
        
    }
    //MARK: - Instance Methods
    func updateModel(color:String){
        myModel.colorFromString(color)
        let newContext:AnyObject? = myModel
        delegate.colorDidFinish(newContext)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel //make the model
        delegate = myModel.delegate as? ColorModalDelegate //unbundle the delegate
        
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

NumberInterfaceController.swift

//
//  NumberInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/5/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol NumberInterfaceDelegate{
    func numberDidFinish(context:Float)
}


class NumberInterfaceController: WKInterfaceController {
    
    //MARK: Outlets and properties
    var number:Float = 0.0
    var delegate:NumberInterfaceDelegate! = nil
    
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    //MARK: - Actions
    @IBAction func sliderDidChange(value: Float) {
        number = value
        updateDisplay()
    }
    
    @IBAction func doneButtonPressed() {
        delegate.numberDidFinish(number)
    }
    //MARK: - Instance Methods
    func updateDisplay(){
        let displayString = String(format:"%0.1f",number)
        statusLabel.setText(displayString)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        // unwrap the context
        let myModel = context as! DemoModel //make the model
        number = myModel.myNumber
        delegate = myModel.delegate as? NumberInterfaceDelegate
        updateDisplay()
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        updateDisplay()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

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 )

Facebook photo

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

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: