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.
Drag the table object to the interface in the storyboard. A group called Table Row appears.
Open the document outline to look at the structure of a table object:
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
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:
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.
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.
From the circles to the left of the outlets, connect the outlets to the controls on the storyboard:
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.
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.
At the bottom, we have finish.
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.
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.
Leave a Reply