Make App Pie

Training for Developers and Artists

Swift WatchKit: Introducing Navigation to the Apple Watch(Part 3: Using Delegates and Contexts)

watch segues and delegates

In our last lesson we set up navigation in the Storyboard and programmatically. We left off with passing data from one view controller to another using the context parameter like this:

   @IBAction func goButtonPressed() {
        //prepare context
        var myContext:Bool? = isDarkColor
        //logical navigtion
        if isGoingToGray {
            pushControllerWithName("Gray", context: myContext)
        }else{
            pushControllerWithName("Color", context: myContext)
        }

    }

We made a context variable which we passed to the destination controller. We haven’t done anything yet with the value of the context. In this lesson, we’ll move  the values between controllers

Open the code we worked with last time. Go to the GrayInterfaceController.swift file. You’ll find there the following method:

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.
    }

You can consider awakeWithContext the viewDidLoad of Watchkit. There is one major difference. The awakeWithContext method has a parameter of context. This is the same context we passed in pushControllerWithName in the last lesson. When the method executes, we can take the passed data and set it up for this method.

We’d like to change the background color  to light to dark. Unfortunately, there is no backgroundColor property or setter – only an attribute on the storyboard. However we can work around this with a very large group. Go to the storyboard and in the object library find group:

2015-05-26_06-10-07

Drag a group onto the gray interface, then drag a group to the Color interface.

2015-05-26_06-14-47

For each of the groups, change the size to Relative to Container for both Width and Height.

2015-05-26_06-17-32

The group now takes all available space. In the Attribute inspector, set the gray group’s color to Light Gray in the attributes inspector. Using the color picker in the background’s attribute inspector, set the color to the  color #440088 we used in the last lesson.

Open the assistant editor.  Select the Gray interface and control-drag from the interface to the  GrayInterfaceController class code:

2015-05-26_06-29-06

Add an outlet named grayBackgroundGroup:

2015-05-26_06-29-40

Do the same for the ColorInterfaceController, naming it colorBackgroundGroup.

Close the assistant editor.  Go to the GrayInterfaceController code again. Add the following three declarations above the outlet you just created:

var isDarkColor = false
    let darkColor = UIColor.darkGrayColor()
    let lightColor = UIColor.lightGrayColor()

We create a property we’ll transfer the context to. Change the awakeWithContext code to this:

@IBOutlet weak var grayBackgroundGroup: WKInterfaceGroup!
    //MARK: -  Life Cycle
        override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
            isDarkColor = context as! Bool //change context into property
        if isDarkColor {
            grayBackgroundGroup.setBackgroundColor(darkColor)
        } else {
            grayBackgroundGroup.setBackgroundColor(lightColor)
        }

    }

This loads the context  into the property, then does the test to find what color to use.

Build and run. You can now select the dark or light color in the gray interface.

2015-05-26_06-56-37 2015-05-26_06-57-15

Let’s Set up similar code in the color interface, but before we do, let’s set up a switch to let us change the color while in the interface.  Start by going back to the storyboard and adding a switch to the Color Interface.

2015-05-26_07-04-15

Open the assistant editor and add an outlet named colorSwitch and an action named colorSwitchDidChange. Once done, close up the assistant editor and go to the ColorInterfaceController code.  Like we did before, add the properties and constants for the controller:

    var isDarkColor = false
    let darkColor = UIColor(red: 68.0/255.0, green: 0.0, blue: 136.0/255.0, alpha: 1.0)
    let lightColor = UIColor(red: 187.0/255.0, green: 1.0, blue: 0, alpha: 1.0)

This time we used two contrasting colors. Add this to awakeWithContext:

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

We’ll use a refresh method  to prevent duplication of code. Add this method:

func refreshDisplay(){
        colorSwitch.setOn(isDarkColor)
        if isDarkColor {
            colorBackgroundGroup.setBackgroundColor(darkColor)
            colorSwitch.setTitle("Dark")
        } else {
            colorBackgroundGroup.setBackgroundColor(lightColor)
            colorSwitch.setTitle("Light")
        }
    }

Build and run. We get similar results but with the switch indicating dark or light.

2015-05-27_07-38-22

Now add the following to the colorSwitchDidChange

@IBAction func colorSwitchDidChange(value: Bool) {
        isDarkColor = value
        refreshDisplay()
    }

Build and run. We can now change from dark to light when we are on the color interface.

2015-05-27_07-38-47

Delegates and Contexts

When we are on the color interface, the result of the switch does not reflect back in the UPInterfaceController. Like navigation controllers in UIKit, we’ll delegate the result back to the UpInterfaceController. WatchKit has a wrinkle in our plans for doing so. There is no destinationViewController property for a WKInterfaceController. There is no way to set the properties of the destination view controller directly. We have passed a single parameter named context to the destination we name in pushControllerWithName like this:

pushControllerWithName("Color", context: myContext)

Context is of type AnyObject? which means we can pass whatever we want through it. If we want multiple values, we can use tuples, dictionaries, structs or classes to pass as many values as we want in our pushController method. In the awakeWithContext in the destination, we break apart the context to distribute properties to the destination. In a separate lesson, we’ll discuss the power and costs of doing each. For this lesson, we’ll make a class and pass a class between the controllers. For example, add above the class declaration for UpInterfaceController the following class:

class DarkOrLight:NSObject{
    var isDarkColor = false
    var count = 0
}

Change the goButtonPressed method to this:

@IBAction func goButtonPressed() {
        //prepare context
        var myContext:Bool? = isDarkColor
        //logical navigtion
        if isGoingToGray {
            var myContext:Bool? = isDarkColor
            pushControllerWithName("Gray", context: myContext)
        }else{
            var myContext = DarkOrLight()
            myContext.isDarkColor = isDarkColor
            myContext.count = 2
            pushControllerWithName("Color", context: myContext)
        }

    }

We did several things here. Depending on the  destination interface,  we have a different context. For the Gray interface, we have a context of a single Bool value. For the Color interface, we pass a object of class DarkOrLight. We assign values to the two properties of DarkOrLight and then send it as the context. The awakeWithContext on the destination controller will take the context and assign its vales to the correct properties. On the destination controller ColorInterfaceController change awakeWithContext to this

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        //convert the context to usuable properties
        let myContext = context as! DarkOrLight
        println(myContext.count)
        isDarkColor = myContext.isDarkColor as Bool
        // update display
        refreshDisplay()
    }

First we cast the context to the correct type. Secondly,  we assign each of the properties of DarkOrLight to values in the ColorInterfaceController. We have no real use for the count in this example so we just printed it to the console to see that it works. If you build and run, you should see no difference between the two codes, except the number 2 showing up in the console.

While we will discuss more about the context in later lessons, our purpose is to use it to set up a delegate to send back values to a previous controller. If you haven’t worked with delegates before, I’d suggest reading my tutorial of them Why Do We Need Delegates?

To set up a delegate, we first make a protocol. Above the ColorInterfaceController declaration add the following code to set up a protocol.

protocol ColorInterfaceDelegate{
    func colorDidChange(value:Bool)
}

In the ColorInterfaceController add a delegate property with the protocol for a type.

var delegate:ColorInterfaceDelegate? = nil

In our action colorSwitchDidChange, add the delegate calling the protocol’s method.

    @IBAction func colorSwitchDidChange(value: Bool) {
        isDarkColor = value
        refreshDisplay()
        delegate?.colorDidChange(value)
    }

Go over to the UpInterfacecController class. In the class declaration adopt the delegate

class UpInterfaceController: WKInterfaceController,ColorInterfaceDelegate {

Xcode almost immediately gives the error about the protocol not being implemented. Add the required method to your code:

func colorDidChange(value:Bool) {
    isDarkColor = value
    println(isDarkColor)
    updateColorSwitch()
 }

We used a new method updateColorSwitch in our code to prevent repetition of code. We’ll need to add it and we’ll change our action method to use it too. Change your code to this:

    func updateColorSwitch(){
        colorSwitch.setOn(isDarkColor)
        if isDarkColor{
            colorSwitch.setTitle("Dark")
        }else{
            colorSwitch.setTitle("Light")
        }
    }

    @IBAction func colorSwitchDidChange(value: Bool) {
        isDarkColor = value
        updateColorSwitch()
    }

The last step is to assign the current view controller to the delegate. Here’s where we use the context. In the goButtonPressed action, add myContext.delegate = self just under the count assignment.

@IBAction func goButtonPressed() {
        //prepare context
        var myContext:Bool? = isDarkColor
        //logical navigtion
        if isGoingToGray {
            var myContext:Bool? = isDarkColor
            pushControllerWithName("Gray", context: myContext)
        }else{
            var myContext = DarkOrLight()
            myContext.isDarkColor = isDarkColor
            myContext.count = 2
            myContext.delegate = self  // <-- add the delegate
            pushControllerWithName("Color", context: myContext)
        }

    }

Now we have to assign it to the property in the destination. add the line delegate = myContext.delegate as? ColorInterfaceDelegate to the awakeWithContext code in the ColorInterfaceController.

override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        //convert the context to usuable properties
        let myContext = context as! DarkOrLight
        delegate = myContext.delegate as? ColorInterfaceDelegate //<-- add delegate
        let count = myContext.count
        isDarkColor = myContext.isDarkColor as Bool
        // update display
        refreshDisplay()
    }

The way things are set up, we need to cast the delegate to ColorInterfaceDelegate to make this work.

You could run the app now. However it will look like it isn’t working. The console  does tell us that the delegate is changing the values. If we were only passing values to the original controller, this would be all we need to do. However we changed the user interface by changing the position and title of the switch. In WatchKit, you have only one method willActivate to update the view before presentation. This is the equivalent of the viewWillAppear in UIKit. Change the willActivate code in the UpViewController to this:

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

Build and run. When we change colors in the color interface, then go back to Up, it reflects in the switches.

watch segues and delegates

This is great if you are just updating from a programmatic button. We still don’t know how to do so with a segue or how to dismiss an interface on the press of a button. We’ll cover this in our next lesson.

The Whole Code

UpInterfaceController.swift

//
//  UpInterfaceController.swift
//  SwiftNavigationDemo
//
//  Created by Steven Lipton on 5/21/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class DarkOrLight:NSObject{
    var isDarkColor = false
    var count = 0
    var delegate:AnyObject? = nil
}

class UpInterfaceController: WKInterfaceController,ColorInterfaceDelegate {

    //MARK: - Outlets
    var isGoingToGray = true
    var isDarkColor = true
    
    @IBOutlet weak var colorSwitch: WKInterfaceSwitch!
    @IBOutlet weak var navigationSwitch: WKInterfaceSwitch!
    //MARK: -  Actions
   
    @IBAction func navigationSwitchDidChange(value: Bool) {
        if value{
            navigationSwitch.setTitle("Gray")
        }else{
            navigationSwitch.setTitle("Color")
        }
        isGoingToGray = value
    }
  
    func colorDidChange(value:Bool) {
        isDarkColor = value
        print(isDarkColor)
        updateColorSwitch()
        //popController()
        
    }
    
    func updateColorSwitch(){
        colorSwitch.setOn(isDarkColor)
        if isDarkColor{
            colorSwitch.setTitle("Dark")
        }else{
            colorSwitch.setTitle("Light")
        }
    }
    
    @IBAction func colorSwitchDidChange(value: Bool) {
        isDarkColor = value
        updateColorSwitch()
    }
 
    @IBAction func goButtonPressed() {
        //prepare context
        var myContext:Bool? = isDarkColor
        //logical navigtion
        if isGoingToGray {
            var myContext:Bool? = isDarkColor
            pushControllerWithName("Gray", context: myContext)
        }else{
            var myContext = DarkOrLight()
            myContext.isDarkColor = isDarkColor
            myContext.count = 2
            myContext.delegate = self  // <-- add the delegate
            pushControllerWithName("Color", context: myContext)
        }
        
    }
    
    //MARK: - Life Cycle
    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()
        updateColorSwitch()
    }

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

}

GrayInterfaceController.swift

//
//  GrayInterfaceController.swift
//  SwiftNavigationDemo
//
//  Created by Steven Lipton on 5/21/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class GrayInterfaceController: WKInterfaceController {
    
    //MARK: -  Properties, Constants and Outlets
    var isDarkColor = false
    let darkColor = UIColor.darkGrayColor()
    let lightColor = UIColor.lightGrayColor()

    @IBOutlet weak var grayBackgroundGroup: WKInterfaceGroup!
    //MARK: -  Life Cycle
        override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
            isDarkColor = context as! Bool //change context into property
        if isDarkColor {
            grayBackgroundGroup.setBackgroundColor(darkColor)
        } else {
            grayBackgroundGroup.setBackgroundColor(lightColor)
        }
        
       
    }

    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()
    }

}

ColorInterfaceController.swift

//
//  ColorInterfaceController.swift
//  SwiftNavigationDemo
//
//  Created by Steven Lipton on 5/21/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol ColorInterfaceDelegate{
    func colorDidChange(value:Bool)
}

class ColorInterfaceController: WKInterfaceController {

    var isDarkColor = false
    let darkColor = UIColor(red: 68.0/255.0, green: 0.0, blue: 136.0/255.0, alpha: 1.0)
    let lightColor = UIColor(red: 187.0/255.0, green: 1.0, blue: 0, alpha: 1.0)
    var delegate:ColorInterfaceDelegate? = nil
    
    @IBOutlet weak var colorSwitch: WKInterfaceSwitch!
    @IBOutlet weak var colorBackgroundGroup: WKInterfaceGroup!
    
    
    @IBAction func colorSwitchDidChange(value: Bool) {
        isDarkColor = value
        refreshDisplay()
        delegate?.colorDidChange(value)
    }
    
    func refreshDisplay(){
        colorSwitch.setOn(isDarkColor)
        if isDarkColor {
            colorBackgroundGroup.setBackgroundColor(darkColor)
            colorSwitch.setTitle("Dark")
        } else {
            colorBackgroundGroup.setBackgroundColor(lightColor)
            colorSwitch.setTitle("Light")
        }
    }
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        //convert the context to usuable properties
        let myContext = context as! DarkOrLight
        delegate = myContext.delegate as? ColorInterfaceDelegate //<-- add delegate
        let count = myContext.count
        isDarkColor = myContext.isDarkColor as Bool
        // update display
        refreshDisplay()
    }

    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()
    }

}

2 responses to “Swift WatchKit: Introducing Navigation to the Apple Watch(Part 3: Using Delegates and Contexts)”

  1. […] be using the same storyboard from the previous lesson and expanding it to match the illustration above. Open the project and go to the storyboard for the […]

  2. […] for a color name, and a UIColor  as data. We also have delegate as AnyObject? As we discussed with delegates for the navigation interface, we’ll downcast the delegate in  the awakeFromContext method of the destination […]

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: