Swift Watchkit: How to Add Simple Dynamic Tables to Apple Watch

2015-08-02_21-09-27

In the last post, we looked at scroll views and static tables. There are many instances where tables filled with data at runtime are necessary.

If you are familiar with UITableViewController, you may be delighted to know that WKInterfaceTable is a lot simpler to put together: there are no delegate or data sources involved. It’s a very different experience than the iOS equivalent.  It’s  shrinks to one method the developer need to implement.

For our example, we’re going to use some data about running. In trying to get rid of all those calories of all that pizza I eat, I do run.  Back in November I ran the Hot Chocolate 15k in Chicago. I was able to collect the data about my run.  In this lesson, we’ll take some of that running data and display the distance and pace achieved for each mile in a table using a single row controller. In the next lesson we’ll add more data and compute my stats at the splits which we’ll present on a separate screen. We’ll learn in the second part how to add and delete data from the table as necessary.  Our last lesson in the series will add 5K kilometer splits and summary information using more than one row controller.

Set Up the Project

Make new project called SwiftWatchKitTable, with Swift as the language and either Universal or iPhone for the device. Save the project.

Once the project loads, select Edit>New Target from the drop down menu. Add a WatchKit App. You will not need a notification for this project, so you can turn that off. Make sure the language is Swift. Click Finish, and then Activate.

Configure the Storyboard

Go to the Storyboard in the WatchKit App group.  In the object catalog, find the table object.

2015-07-30_05-26-25

Drag the table object to the interface in the storyboard. A group called Table Row appears.

2015-07-30_05-29-10

Open the document outline to look at the structure of a table object:

2015-07-30_05-32-01

A table has one or more table row controllers.  In each row  controller is a group for adding controls. Most typically labels or images, but we can add buttons.

Tables can have more than one row controller.  You may have one row controller for the data, one for a header and one for a footer for example. In this lesson, we’ll use only one. In the third part of this series on tables, we’ll work with multiple row controllers.

Click on the Table Row Controller in the document outline. Row controllers need unique names in the table. In the attribute inspector, make the identifier row

2015-07-30_05-39-09

Click the group for the row controller. By default, this group is horizontal.  For our app, set the Layout to  Vertical. Set the Height Size to Size to Fit Content. Drag two labels into the group. Title them like this:

2015-07-30_05-42-44

 

Make a Row Controller

Our next step in building a table is making a table row controller.  Tap Command-F to make a new file.  Create a new Cocoa Touch Class named TableRowController. Subclass NSObject for this file. Save the file in the WatchKit extension group. Be careful that Xcode does not default you to the app.

Since this is a WatchKit project and not an iOS one, change

import UIKit

to

import Watchkit

Now we can use the correct classes.

Row controllers don’t do a lot. Typically they contain the outlets for the controls in the row controller.  In our example, add two outlets for WKInterfaceLabel, splits and time.

class TableRowController: NSObject {
    @IBOutlet weak var splits: WKInterfaceLabel!
    @IBOutlet weak var time: WKInterfaceLabel!
}

Nothing exciting here.  Just two  outlets. You can add actions, but for our basic example we’ll stick to just outlets. We’ll discuss actions in row controllers in the second part of the series.

Connect a Row Controller

Now we connect our row controller to the table. Start by going to the storyboard. Click on the row controller named row in the document outline. In the identity inspector, change the Class to TableRowController.

2015-07-30_06-07-07

Now we open the assistant editor. Xcode assumes you want to work with the interface. Most likely you will see the InterfaceController.swift file in the assistant editor. Click at the top where it says Automatic and select Manual. Select through the choices to get the Watchkit Extension file TableRowController.swift.

2015-07-31_06-42-47

From the circles to the left of the outlets, connect the outlets to the controls on the storyboard:

2015-07-30_06-11-00

We’ve now connected the row controller to the storyboard. You can close the assistant editor to make room.

Add a Model

Like UITableView, the typical data for a WKInterfaceTable is an array.  I’m making a simple constant array for this of my pace measured in seconds during the Hot Chocolate 15K race (How I gathered this data click here).  Go to the InterfaceController.swift file. Add this to the InterfaceController  class.

    var data = [654,862,860,802,774,716,892,775,748,886,835]

This the seconds of my current pace at the mile markers. Each data point is one mile away from the next one.  For this example, this avoids adding classes or dictionaries for the data, and thus keep things as simple as possible.

Pace data as an integer is not very user friendly.  Since I’m using an Int for my data and not NSTimeInterval I can’t use time formatters.   There is an easy solution the make a string to display the data we need. Add the following function to the InterfaceController Class:

    func paceSeconds(pace:Int) -> String{
        let minutes = pace / 60
        let seconds = pace % 60
        return String(format:"00:%02i:%02i",minutes,seconds)
    }

Since I’m using integers, dividing by 60 seconds in a minute gives me the number of minutes. Taking the mod of 60 seconds give me the remaining seconds. I just format that in a string and am done.

Implement the Table

Select the storyboard, open up the assistant editor again, and set back to Automatic to get the InterfaceControlle.swift file in the assistant editor. Control-drag from the table control in the document outline to the code to make an outlet. Label the outlet table.

 @IBOutlet weak var table: WKInterfaceTable!

Close the assistant editor, and select the interface controller. We will write one method to load and refresh the table. Start by making the method in InterfaceController.

func tableRefresh(){
}

Watchkit has no delegates and data sources for tables. everything happens in this method. Instead of a data source, we tell the table how many rows there are in the table, and what row controllers to use with one of two methods in WKInterfaceTable. In the simple cases of one row controller, we use setNumberOfRows(rows:, withRowType:). Add this to the tableRefresh method.

table.setNumberOfRows(data.count, withRowType: "row")

The parameter rows is the number of rows in the table. We count the elements in the array with data.count. Row types are the type of row controller we used. That is the identifying string for the row controller we set earlier named row.

We’ve now made an empty table. We’ll make a loop to populate the table. Add this to the code.

for var index = 0; index < table.numberOfRows; index++ {

}

This will loop through all the elements in the array. We’ll use index to reference a row on a table. We need  index as a data point too — it’s the number of miles run.
In our loop, we get the row from the table. Add this code to the loop

let row = table.rowControllerAtIndex(index) as! TableRowController

We get the row at the current index, then cast it correctly to a TableViewController. Populate the row controller row with data:

var rowString = String(format: "Split:%02i miles", index + 1)
let paceString = "Pace:" + paceSeconds(data[index])
row.splits.setText(rowString)
row.time.setText(paceString)

Our full method should look like this

func tableRefresh(){
    table.setNumberOfRows(data.count, withRowType: "row")
    for var index = 0; index < table.numberOfRows; index++ {
        let row = table.rowControllerAtIndex(index) as! TableRowController
        var rowString = String(format: "Split:%02i miles", index + 1)
        let paceString = "Pace:" + paceSeconds(data[index])
        row.splits.setText(rowString)
        row.time.setText(paceString)
    }
}

Finally, call the refresh method when we start the watch app. Change willActivate() to

override func willActivate() {
        super.willActivate()
        tableRefresh()
    }

It’s a a good practice to refresh the interface at the very last moment before activation to keep it accurate. That’s why we use willActivate instead of awakeWithContext.
Build and run. You should have a working table.

2015-07-31_07-05-06

A Few Cosmetic Changes

There’s two cosmetic changes we need to make. The first is to use mile instead of miles in the first table entry. The second change is to change how we handle the last entry in the table. Races and runs rarely end exactly at a mile marker. Most races are actually metric lengths, so If very unlikely that will. We will have a fraction of a mile as the last entry in the table. Given we do not have data for exactly how far we’ve run, we change rowString to Finish instead. Add this to the code fir the loop, above the setText method calls

if index == (table.numberOfRows - 1){ //table end
    rowString = "Finish"
}
if index == 0 {
    rowString = "Split:01 mile" //Table beginning
}

Build and run. At the top we have mile.

2015-07-31_07-08-45

At the bottom, we have finish.

2015-07-31_07-08-17

Scroll to a Row

You can programmatically scroll to a row. In our app, we might want to see the finish instead of the start first. The method scrollToRowAtIndex does this. Add this code just after the block of the for loop:

table.scrollToRowAtIndex(table.numberOfRows - 1)

Build and Run. Our code now starts the app displaying the end of the table.

2015-07-31_07-08-17

With this much code , you can have a working table in WatchKit. However it’s not very interactive or sophisticated. We’ll have two more lessons on tables in WatchKit. In the next lesson, we’ll make things interactive and use a more robust model for our data. In the third lesson, we’ll add headers, footers and different types of body rows to our table.

The Whole Code

InterfaceController.swift

//
//  InterfaceController.swift
//  SwiftWatchKitTable WatchKit Extension
//
//  Created by Steven Lipton on 7/31/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {
//MARK: Outlets and properties
    @IBOutlet weak var table: WKInterfaceTable!
//data is the pace in seconds per mile, taken every one mile except the last data point.
    var data = [654,862,860,802,774,716,892,775,748,886,835]
//A function to change the seconds data from an integer to a string in the form 00:00:00
// Not implementing for times over 59:59 min/mi, since that is not a practiacl speed for this app.
    func paceSeconds(pace:Int) -> String{
        let minutes = pace / 60
        let seconds = pace % 60
        return String(format:"00:%02i:%02i",minutes,seconds)
    }

// The table creation method
// WatchKit replaces all the delegates in UITableViewController
// with a developer defined function.
    func refreshTable(){
//Set number of rows and the class of the rows
        table.setNumberOfRows(data.count, withRowType: "row")
//Loop through the rows of the table and populate them with data
        for var index = 0; index < table.numberOfRows; index++ {

            let row = table.rowControllerAtIndex(index) as! TableRowController //get the row
            var rowString = String(format: "Split:%02i miles", index + 1)
            let paceString = "Pace:" + paceSeconds(data[index])
            if index == (table.numberOfRows - 1){ //Table End Handler
                 rowString = "Finish"
            }
            if index == 0 {
                rowString = "Split:01 mile" //Table Beginning Handler
            }
//Set the properties of the row Controller.
            row.splits.setText(rowString)
            row.time.setText(paceString)
        } //end loop
//Scroll to last table row.
        table.scrollToRowAtIndex(table.numberOfRows - 1)
    }

    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()
        refreshTable() //make the table
    }

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

}

TableRowController.swift

//
//  TableRowController.swift
//  SwiftWatchkitTableDemo
//
//  Created by Steven Lipton on 7/29/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class TableRowController: NSObject {
//Typical Outlets to use in the row controller
    @IBOutlet weak var splits: WKInterfaceLabel!
    @IBOutlet weak var time: WKInterfaceLabel!
}

A Footnote About the Data.

For those wondering about this data, it’s my real data from the Hot Chocolate 15K race in Chicago, which will be the data set I use for the entire series. The data collected by the app is in 1/100 of a mile increments, which I reduced to two smaller data sets, one of just miles, and one of tenths of a mile, which we’ll need for the last part of the table series and the map post that comes after that one.  I recorded the data on RunMeter by Abvio, which is one of the apps I use for running on my Phone and now my watch. As running apps go, Runmeter is for power users. It has more features than one can imagine,  though most useful for me is audible and vibrating calling out of intervals during some of my interval practice runs. It also has the export features which helps me a lot in getting data for this lesson, which is why this lesson uses Runmeter data. Its downside is there are so many features its easy to get lost in their menus.

 

2 Replies to “Swift Watchkit: How to Add Simple Dynamic Tables to Apple Watch”

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s