Today we’re going to show you how to use a WKInterfaceTable
in your Apple Watch applications. If you’ve been following or reading this blog for any period of time you will know that we’re passionate about home automation controls, and look forward to HomeKit in the market. The Apple Watch interface will lend itself well to controlling home automation devices as it will eliminate things like digging your phone out of your pocket to unlock the door. Bring up your Apple Watch, open your home automation app, and press Unlock.
Let’s get started! Head on over to Bitbucket and download the starter app.
Our starter app already has an Apple Watch target provided and supplementary images. Open the watchkittables.xcodeproj
project in Xcode 6.2 or higher (this is the first version with iOS 8.2 and WatchKit).
Home automation control is all about controlling accessories, and what better way to quickly view all of your accessories than with a table? WatchKit provides a table UI element with WKInterfaceTable
. We’re going to create a table with two types of rows. One row type will contain a lightbulb image and a button to toggle it on and off. The other row type will contain a label and two buttons to increase and decrease the setpoint temperature on a thermostat.
Bring up your Watch App Interface.storyboard and drag and drop a Table onto the watch canvas. Note that the description for a WKInterfaceTable
says “Displays one or more rows of data”. After this step your watch interface should look like this:
We’re going to stop and make the distinction now between a table and a row in WatchKit. In the screenshot above note that the Interface Controller contains a Table, and that Table contains a Table Row Controller. The Table in WatchKit is the UI component which will manage the rows, and the row controller will manage the display of a type of row. If you have one more than one type of row you will have more than one row controller.
The table row controller implementation is in the extension (in this first iteration of WatchKit it should pointed out that implementation is always in the extension) and is a separate Swift class.
Now, we’ve established that we want two types of rows and we currently only have one, so click on the Attributes inspector for the table (not the row controller) and set the Rows value to 2:
Notice that there are now two Table Row Controllers. Each table row controller is responsible for managing their type of row.
We will have two row types:
- LightbulbAccessoryRow
- ThermostatAccessoryRow
Click on the first Table Row Controller and in the Attributes inspector for Identifier type LightbulbAccessoryRow. Select the second Table Row Controller and for Identifier type ThermostatAccessoryRow.
Now let’s define the content of the LightbulbAccessoryRow.
In Interface.storyboard click on the group inside the LightbulbAccessoryRow and drag-and-drop an image and button inside the group.
Set the image size to 32×32 and for the button set the size attributes to Size to Fit Content.
Set the horizontal position of the button to Center.
We now need to create a Swift class for our table row. In the project navigator click on watchkittables WatchKit Extension and then go to File – New – File and choose an iOS Swift file. Name the file LightbulbAccessoryRow and ensure it is in the watchkittables WatchKit Extensions folder, group, and target.
The table row class must inherit from NSObject
and you will want to add import WatchKit
since this class will contain WatchKit objects:
1 2 3 4 5 6 7 |
import Foundation import WatchKit class LightbulbAccessoryRow : NSObject { } |
Now let’s wire our WatchKit UI elements to the row class. Click on Interface.storyboard in the Watch app and select the LightbulbAccessoryRow and then reveal the Identity Inspector and change the row class to LightbulbAccessoryRow.
Now, select the image in our LightbulbAccessoryRow. Click on the Assistant editor button (the joined rings). If InterfaceController.swift
appears, change it to LightbulbAccessoryRow.swift
.
CTRL-click the image and drag and drop an Outlet into the LightbulbAccessoryRow
class, naming the attribute lightbulbImage
.
Now add an outlet for the button (lightbulbButton
) and an Action named buttonTapped
).
Recall that when your Watch app is instituted the init
method of your InterfaceController
will be called. Here is where we will initialize our table, so create an outlet for your table in InterfaceController
.
We’ve named our table accessoryTable
.
In the init
method of InterfaceController
add the following (after the call to the super initializer of course):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let accessories = [ ["type":"lightbulb", "name":"Living Room", "state":"off"], ["type":"lightbulb", "name":"Dining Room", "state":"on"], ["type":"thermostat", "name":"Upstairs", "mode":"cool", "setpoint":"72"], ["type":"thermostat", "name":"Downstairs", "mode":"cool", "setpoint":"68"] ] |
To simplify the tutorial we’ll use this predefined data to drive our table. In the real world this data will come from your iOS application. In our case, we would query the state of the HomeKit environment.
To add rows for our lightbulbs we will use:
1 2 |
let lightbulbs = accessories.filter{$0["type"] == "lightbulb"} self.accessoryTable.setNumberOfRows(lightbulbs.count, withRowType: "LightbulbAccessoryRow") |
Let’s stop and explain these two lines. The first is straightforward (if you are familiar with the Array filter
function that is); we are filtering our accessory Array
for those accessories whose type
key is set to lightbulb
. The second line is where we tell our table to provide us with a set of number of LightbulbAccessoryRow
rows. Note that the rowType
is given as a String
. This is the identifier string you provided in the Attributes inspector for the row. The string given in the code and that given in the interface builder must match.
Now that we’ve set the number of rows, let’s add them. This can be accomplished with:
1 2 3 4 5 6 7 8 9 10 11 12 |
for var i = 0; i < lightbulbs.count; i++ { let row = self.accessoryTable.rowControllerAtIndex(i) as LightbulbAccessoryRow row.lightbulbImage.setImageNamed(lightbulbs[i]["state"]) if lightbulbs[i]["state"] == "off" { row.lightbulbButton.setTitle("On") } else { row.lightbulbButton.setTitle("Off") } } |
You can run the application at this point and get a watch app that looks like this:
Now, we want to add an implementation for buttonTapped
. Unfortunately WatchKit does not provide for querying the attributes of elements on the watch display in your extension app. For example, we cannot say:
1 2 3 |
if lightbulbButton.title == "Off" { lightbulbButton.setTitle("On") } |
One can assume Apple did this for a reason, and thats to minimize the amount of communication traffic between the Watch and iPhone.
We’ll have to add our own state variable. In the LightbulbAccessoryRow
class add a boolean flag:
1 |
var on:Bool = false |
Then in the buttonTapped
method of LightbulbAccessoryRow
use the boolean flag to change the image and title depending on the value. Make sure and toggle the bool value as well.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@IBAction func buttonTapped() { if self.on { // Turn bulb off self.lightbulbImage.setImageNamed("off") self.lightbulbButton.setTitle("Turn On") self.on = false } else { // Turn bulb on self.lightbulbImage.setImageNamed("on") self.lightbulbButton.setTitle("Turn Off") self.on = true } } |
Update the row initialization loop code in InterfaceController
to make use of the property:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
for var i = 0; i < lightbulbs.count; i++ { let row = self.accessoryTable.rowControllerAtIndex(i) as LightbulbAccessoryRow row.lightbulbImage.setImageNamed(lightbulbs[i]["state"]) if lightbulbs[i]["state"] == "off" { row.on = false row.lightbulbButton.setTitle("Turn On") } else { row.on = true row.lightbulbButton.setTitle("Turn Off") } } |
Run the Watch app in the Xcode simulator, and your app should run similar to that in this video:
[youtube]http://youtu.be/3h3V2D4Q29g[/youtube]
I can hear the howls of protest regarding how we’ve implemented the state nature of our lightbulb! For extra credit, yes, you can implement functions in the LightbulbAccessoryRow
class for setOn
and setOff
which encapsulate all of the logic of setting images, strings, and flags.
Now let’s add our support for our ThermostatAccessoryRow
. Create a new Swift file ThermostatAccessoryRow
and ensure that it is in the extension folder, group, and target. We’ll make the point once more: your Swift file should include import WatchKit
and your row class needs to inherit from NSObject
:
1 2 3 4 5 6 |
import Foundation import WatchKit class ThermostatAccessoryRow : NSObject { } |
Now go to the Interface.storyboard and click on the ThermostatAccessoryRow and in the Identity Inspector set the class to ThermostatAccessoryRow
. This should be old hat by now.
To give our UI a little flair we’ll add two groups to our existing default ThermostatAccessoryRow group. The first group will contain our thermostat cooling setpoint temperature label, and the second will contain the up and down buttons for the setpoint. While writing this tutorial I decided that verbally describing every setting in Interface Builder would be cumbersome, so you can see from this video how the layout for the ThermostatAccessoryRow is accomplished.
[youtube]https://www.youtube.com/watch?v=rX90egdWd_8[/youtube]
Briefly, the thermostat row has two groups: the first group contains our labels and is layed out vertically. Our second group contains the up and down buttons. The buttons don’t have any titles, but instead we’ve used the cool_down
and cool_up
images that were included in the Images.xcassets folder. You can if you like, name the groups in Xcode for ease-of-use. We’ve named ours Setpoint Temperature and Setpoint Controls, respectively.
For the purposes of this tutorial if you don’t get the exact table row layout right, it’s okay. You can even skip adding the °F label, it’s only for effect.
In case you missed it, we wired up an outlet (setpointTemperatureLabel
) and two actions: one for the down button (setpointDown()
) and one for the up button (setpointUp()
).
An aside: If you ever see the error Could not insert new outlet connection: Could not find any information for the class named Classname
, this is Interface Builder’s way of saying it hasn’t processed (indexed, scanned, take your pick) the class file in question. Why it hasn’t done so could be any number of reasons, but I’ve found its often the case when I have something running in the simulator. Stop any running simulations with the Xcode stop button and try again. If all else fails quitting Xcode and reopening the project usually works.
Our final ThermostatRowController
UI should look something like:
And, now our ThermostatRowController
class will look like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ThermostatAccessoryRow : NSObject { @IBOutlet weak var setpointTemperatureLabel: WKInterfaceLabel! var setpoint = 72 @IBAction func setpointDown() { self.setpoint = self.setpoint - 1 self.setpointTemperatureLabel.setText("\(self.setpoint)") } @IBAction func setpointUp() { self.setpoint = self.setpoint + 1 self.setpointTemperatureLabel.setText("\(self.setpoint)") } } |
The logic of the row controller is straightforward. As with our lightbulb, we cannot query the label text dynamically so we add a setpoint
property to our class. When the user presses our setpointDown
button we decrement the setpoint and then set the label accordingly. Likewise, when our setpointUp
button is tapped, we increment and set accordingly.
Multiple Row Types
Adding the second type of row controller introduces us to the setRowTypes
function of WKInterfaceTable
. Whereas previously we used setNumberOfRows
for a table containing one type of row, a table containing more than one row type requires us to use setRowTypes
.
setRowTypes
takes an array of String
s, with each entry declaring what type of row controller should govern the row at the given array index. Let that soak in. If you want 5 rows, 3 of which are lightbulbs and 2 of which are thermostats you’re going to create an array of 5 strings:
1 2 3 4 5 6 7 |
let rowTypes = ["LightbulbAccessoryRow", "LightbulbAccessoryRow", "LightbulbAccessoryRow", "ThermostatAccessoryRow", "ThermostatAccessoryRow"] self.accessoryTable.setRowTypes(rowTypes) |
Make sure and spell your row type correctly! Otherwise, you’ll be treated with the incredibly irritating Error - unable to instantiate row controller class (null) for row 0
error!
We’ll build our rowType
array dynamically with the following:
1 2 3 4 5 6 7 |
var rowTypes = [String]() for accessory in accessories { var type = accessory["type"]! type = type.capitalizedString rowTypes.append("\(type)AccessoryRow") } |
First, declare a new array of type [String]
(that is, an array of String
s). Then, for each accessory, obtain the type and use it for building our row controller class name. Append the name to our rowTypes
array.
Stop now and in the InterfaceController
init
function delete all of the previous table initialization code for the lightbulb and replace with the above. Your function should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
override init(context: AnyObject?) { super.init(context: context) let accessories = [ ["type":"lightbulb", "name":"Living Room", "state":"off"], ["type":"lightbulb", "name":"Dining Room", "state":"on"], ["type":"thermostat", "name":"Upstairs", "mode":"cool", "setpoint":"72"], ["type":"thermostat", "name":"Downstairs", "mode":"cool", "setpoint":"68"] ] var rowTypes = [String]() for accessory in accessories { var type = accessory["type"]! type = type.capitalizedString rowTypes.append("\(type)AccessoryRow") } } |
Our logic for populating each row in the table is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
self.accessoryTable.setRowTypes(rowTypes) for var i = 0; i < accessories.count; i++ { if accessories[i]["type"] == "lightbulb" { let row = self.accessoryTable.rowControllerAtIndex(i) as LightbulbAccessoryRow row.lightbulbImage.setImageNamed(accessories[i]["state"]) if accessories[i]["state"] == "off" { row.on = false row.lightbulbButton.setTitle("Turn On") } else { row.on = true row.lightbulbButton.setTitle("Turn Off") } } else if accessories[i]["type"] == "thermostat" { let row = self.accessoryTable.rowControllerAtIndex(i) as ThermostatAccessoryRow let t = accessories[i]["setpoint"] row.setpointTemperatureLabel.setText(t) row.setpoint = t!.toInt()! } } |
There are many ways to improve upon this code, and it’s not meant to necessarily be an example of elegant and DRY techniques. For our training purposes it will suffice.
Run the example again and you should have something very similar to that in our video:
[youtube]https://www.youtube.com/watch?v=HxJjhs3dSiQ[/youtube]
Getting the Code
To get the starter application download it directly from Bitbucket. Or, check out the master branch at Bitbucket for the completed application.
Final Thoughts
If you’re a long time Cocoa and UIKit developer you’ll notice that the table programming paradigm is quite different. With OS X and iOS tables you create table controllers, table data sources, and stick with a “the table will ask you for data” MVC model. Apple Watch table control is (depending upon your point of view) much simpler with a direct “set this row to this data” approach. When you take a look at the WatchKit environment this again makes more sense if it is put in the light of how much communication needs to occur between the watch and phone. With the current WKInterfaceTable
method the watch does not need to constantly ask the phone for row data. It remains to be seen if this will change as application logic moves to the watch in future iterations.
Follow us!
If you’ve liked our tutorial and are interested in keeping up with our posts, follow us on Twitter at @iachievedit!