WatchKit Delegates and Contexts



Limited API

As you use WatchKit you'll soon realize that its API is a lot more limited than UIKit. This is actually a very good thing. Afterall we are programming for a device that fits on your wrist. Interface elements can only be set to values and can't be read from since there is no API to read the values inside the interface controls. The text currently in a label must have state changes tracked separately in code. Another thing you'll realize is there isn't a prepareForSegue method. There is however a contextForSegueWithIdentifier method. The context passed to the next View Controller must be a single object of AnyObject type. It is here we must pass on any data and/or delegate. I found an intuitive way to send both a data object and a delegate.

Context

Let's take a look inside a WKInterfaceController to see how it gets to its own context object.

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

    }
}

Notice in the code above we will be called with the awakeWithContext method after the storyboard is loaded which will give us the context object of type AnyObject. This seems a little less than ideal because now that object has to be casted to the object type we need. Also, it is of type AnyObject instead of type Any so structs won't work, we'll need to cast it to a class instance.

While it seems like we might want to simply send a data object as the context for this controller it wouldn't make sense to have a data object have a controller delegate property. It makes more sense to wrap it in an object that has the data and the delegate. Let's call this class Context.

Let's start with this. There is a problem with this, see if you can figure out what it is:

class Context {
    var object: AnyObject?
    var delegate: AnyObject?
}

While from the looks of this it looks like a general solution to the problem, it has a major flaw. We will not be able to cast the delegate variable to a protocol type like we are used to with delegates. Type Any won't work either. Finding a way to generalize this was actually pretty hard. Let's try making Context a generalized protocol that other context objects can conform to and where they can specify the specific protocol type.

protocol Context {
    typealias DelType
    typealias ObjType
    var delegate: DelType? { get set }
    var object: ObjType? { get set }
}

In this protocol we are are not giving a specific type to the delegate or the object. We will let the conforming class specify these.

class BookControllerContext: Context {
    typealias DelType = BookControllerDelegate
    typealias ObjType = Book
    var delegate: DelType?
    weak var object: ObjType?
}

Since the information given in the context is not just model-specific as it has a controller delegate it makes sense to me to call it ControllerContext.

Pass it along

Now let's look at how we can pass a long a specific context object from one interface controller to another. In this example we'll assume the outer controller is a list of Books and the inner controller is a description screen of 1 specific book.

Here is our model object Book:

class Book {
    var title: String
    var author: String
    var description: String
    var price: Double
    var owned: Bool
    init(title: String, author: String, description: String, price: Double, owned: Bool) {
	self.title = title
	self.author = author
	self.description = description
	self.price = price
	self.owned = owned
    }
}

Here is our Row View:

class BookRow: NSObject {
    @IBOutlet weak var bookTitleLabel: WKInterfaceLabel!
    @IBOutlet weak var bookAuthorLabel: WKInterfaceLabel!
    @IBOutlet weak var bookPriceLabel: WKInterfaceLabel!
    @IBOutlet weak var bookBuyLabel: WKInterfaceLabel!
}

Here is the delegate protocol used for the inner controller to inform the outer controller they bought the book they were viewing:

protocol BookControllerDelegate  {
    func didBuyBook(book: Book)
}

And here is the outer book list controller:

class BookListController: WKInterfaceController, BookControllerDelegate {

    // MARK: BookListController

    @IBOutlet var table: WKInterfaceTable!

    var books = [
	// Pricing based on Amazon.com on Jan 27, 2014
	Book(title: "NSHipster Obscure Topics in Cocoa & Objective-C", author: "Mattt Thompson", description: "To be an NSHipster is to care deeply about the craft of writing code.", price: 25.29, owned: false),
	Book(title: "Functional Programming in Swift", author: "Chris Eidhof, Florian Kugler, Wouter Swierstra", description: "This book will teach you how to use Swift to apply functional programming techniques to your iOS or OS X projects", price: 53.07, owned: false)
    ]

    func updateDisplay() {
	table.setNumberOfRows(books.count, withRowType: "bookRow")

	for i in 0..<table.numberOfRows {
	    var row = table.rowControllerAtIndex(i) as BookRow
	    row.bookTitleLabel.setText(books[i].title)
	    row.bookAuthorLabel.setText(books[i].author)
	    row.bookPriceLabel.setText("\(books[i].price)")
	    if books[i].owned {
		row.bookBuyLabel.setText("Read")
	    } else {
		row.bookBuyLabel.setText("Buy")
	    }
	}
    }

    // MARK: WKInterfaceController
    override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
	var context = BookControllerContext()
	context.object = books[rowIndex]
	context.delegate = self
	return context
    }

    override func willActivate() {
	super.willActivate()

	updateDisplay()
    }

    // MARK: BookControllerDelegate
    func didBuyBook(book: Book) {
	book.owned = true
	updateDisplay()
    }
}

Notice above that it is the contextForSegueWithIdentifier method that allows us to create the context object and return it. Now let's look at how the inner controller retrieves it and sets its own delegate:

class BookController: WKInterfaceController {

    var delegate: BookControllerDelegate?
    var book: Book!

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

	let ctx = context as BookControllerContext
	book = ctx.object
	delegate = ctx.delegate
    }

    @IBOutlet weak var buyBtn: WKInterfaceButton!
    @IBAction func buyPressed() {
	delegate?.didBuyBook(book)
	popController()
    }
}

Full Source Example

Here is all the code put together:

import WatchKit

/*
 * Model
 */
protocol Context {
    typealias DelType
    typealias ObjType
    var delegate: DelType? { get set }
    var object: ObjType? { get set }
}

class BookControllerContext: Context {
    typealias DelType = BookControllerDelegate
    typealias ObjType = Book
    var delegate: DelType?
    weak var object: ObjType?
}

class Book {
    var title: String
    var author: String
    var description: String
    var price: Double
    var owned: Bool
    init(title: String, author: String, description: String, price: Double, owned: Bool) {
	self.title = title
	self.author = author
	self.description = description
	self.price = price
	self.owned = owned
    }
}

/*
 * View
 */
class BookRow: NSObject {
    @IBOutlet weak var bookTitleLabel: WKInterfaceLabel!
    @IBOutlet weak var bookAuthorLabel: WKInterfaceLabel!
    @IBOutlet weak var bookPriceLabel: WKInterfaceLabel!
    @IBOutlet weak var bookBuyLabel: WKInterfaceLabel!
}

/*
 * Controller
 */
protocol BookControllerDelegate  {
    func didBuyBook(book: Book)
}

class BookController: WKInterfaceController {

    var delegate: BookControllerDelegate?
    var book: Book!

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

	let ctx = context as BookControllerContext
	book = ctx.object
	delegate = ctx.delegate
    }

    @IBOutlet weak var buyBtn: WKInterfaceButton!
    @IBAction func buyPressed() {
	delegate?.didBuyBook(book)
	popController()
    }
}

class BookListController: WKInterfaceController, BookControllerDelegate {

    // MARK: BookListController

    @IBOutlet var table: WKInterfaceTable!

    var books = [
	// Pricing based on Amazon.com on Jan 27, 2014
	Book(title: "NSHipster Obscure Topics in Cocoa & Objective-C", author: "Mattt Thompson", description: "To be an NSHipster is to care deeply about the craft of writing code.", price: 25.29, owned: false),
	Book(title: "Functional Programming in Swift", author: "Chris Eidhof, Florian Kugler, Wouter Swierstra", description: "This book will teach you how to use Swift to apply functional programming techniques to your iOS or OS X projects", price: 53.07, owned: false)
    ]

    func updateDisplay() {
	table.setNumberOfRows(books.count, withRowType: "bookRow")

	for i in 0..<table.numberOfRows {
	    var row = table.rowControllerAtIndex(i) as BookRow
	    row.bookTitleLabel.setText(books[i].title)
	    row.bookAuthorLabel.setText(books[i].author)
	    row.bookPriceLabel.setText("\(books[i].price)")
	    if books[i].owned {
		row.bookBuyLabel.setText("Read")
	    } else {
		row.bookBuyLabel.setText("Buy")
	    }
	}
    }

    // MARK: WKInterfaceController
    override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? {
	var context = BookControllerContext()
	context.object = books[rowIndex]
	context.delegate = self
	return context
    }

    override func willActivate() {
	super.willActivate()

	updateDisplay()
    }

    // MARK: BookControllerDelegate
    func didBuyBook(book: Book) {
	book.owned = true
	updateDisplay()
    }
}

Conclusion

So you saw how to pass along both data and a delegate to an interface controller in WatchKit. We can conform new contexts to the Context protocol, like if we had an author interface controller and needed to pass it an AuthorControllerContext object containing an Author data object as well as an AuthorControllerDelegate. This is the best way I've found for now to work around some of the limitations with WatchKit. I'm sure there are other and even better ways to do this. If you have any ideas let me know because I really want to know!

Author: Korey Hinton

Created: 2015-02-03 Tue 13:40

Emacs 24.4.1 (Org mode 8.2.10)

Validate