A Look at Apple WatchKit Tables

Categories:

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:

table_tut_basictablerow

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:

table_tut_initial

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.

table_tut_named_rows

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.

table_tut_lightbulbaccessoryrow

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.

table_tut_addlightbulbswift

The table row class must inherit from NSObject and you will want to add import WatchKit since this class will contain WatchKit objects:

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.

table_tut_changeclass

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.

table_tut_linkimage

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).

table_tut_lightbulbrowwired

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.

table_tut_addtableoutlet

We’ve named our table accessoryTable.

In the init method of InterfaceController add the following (after the call to the super initializer of course):

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:

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:

You can run the application at this point and get a watch app that looks like this:

table_tut_firstwatch

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:

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:

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.

Update the row initialization loop code in InterfaceController to make use of the property:

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:

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.

table_tut_cannotinsert

Our final ThermostatRowController UI should look something like:

table_tut_thermostatrow

And, now our ThermostatRowController class will look like this

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 Strings, 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:

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:

First, declare a new array of type [String] (that is, an array of Strings). 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:

Our logic for populating each row in the table is as follows:

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!

Leave a Reply

Your email address will not be published. Required fields are marked *