Tag Archives: table

The Complete Table Tutorial for WatchOS2

In last week’s tutorial of this series, we made a simple dynamic table for the Apple Watch and demonstrated static scrolling tables.Based on some running pace data, we displayed the pace I ran at the mile splits. Many apps will need more than what we discussed. Apps might navigate from the table, insert and delete entries and select entries. In this lesson we’ll also introduce menus and text input.

Make a New Project

We’ll review what we did in the basic tables lesson by rebuilding the application with a few changes. If you built that app, you can modify it and save yourself some time. Along the way I’ll We’ll leave out some of the explanation this time, so if you want more clarification go to the last lesson. Make a new project using the WatchOS application template iOS App with WatchOS App Name the project SwiftWatchOS2TableDemo, with Swift as the language and Universal for the device, clicking off Include Notification Scene. Save the project. Start your watch and phone simulators to get them ready to run.

Add Your Controls

On the Interface.storyboard, add two buttons and a label. Make the button backgrounds Medium Blue(#8080FF). Title the top button Bottom and the bottom button Top.

2016-03-17_07-28-43

In the object catalog, find the table object.

2015-07-30_05-26-25

Drag the table object to the interface in the storyboard, inserting it between the two buttons. A group called Table Row appears.

2016-03-17_07-31-48

Add two labels to the bottom of the watch interface. The interface will expand as you add them in. Make their text look like this

2016-03-24_07-17-17

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

2016-03-24_06-57-45

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:

2016-03-24_07-16-35

Go to the storyboard and drag out an interface. Drag two labels to the interface. On the upper label, change the color to Yellow(#FFFF00), and a title of Average Pace. Make both labels have a width Relative to Container. Make the white one Align Center, with a title of 00:00:00. Click the view controller icon and set the title and identifier to Info. When done, you should have an interface that looks like this one.

2016-03-24_07-14-52

Adding and Connecting View Controller Classes

We have three view controllers we’ll need to hook up. We have the new Info interface, the row controller and the main InterfaceController

Connecting the Info controller

Add a new file by pressing Command-N. Make a WatchKit Class that subclasses WKInterfaceController called InfoInterfaceController. Be sure it is saved in the extension group and not the app group by clicking the drop down menu on the save menu.

2016-03-24_07-25-52

Go to the storyboard. Click the view controller icon on the Info interface. In the Identity inspector, set the Class to InfoInterfaceController. Open the assistant editor. Control drag from the Info Interface to the code to make this outlet:

 @IBOutlet var paceLabel: WKInterfaceLabel!

Connecting the Table Row Controller

Tap Command-N to make a new file. Create a new WatchKit Class named TableRowController. Subclass NSObject for this file. Save the file in the WatchKit Extension Group. Row controllers don’t do a lot. Typically they contain the outlets for the controls in the row controller. In the code that appears, add two outlets for WKInterfaceLabel, splits and time.

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

Go to the storyboard. Click on the row controller named row in the document outline. In the identity inspector, change the Class to TableRowController. 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 by dragging from the circle next to the code to the control. We’ve now connected the row controller to the storyboard.

Setting the View Controller

Change the assistant editor back to automatic and InterfaceController.swift should be back in the assistant editor. Add the following outlets and actions to the code:

@IBOutlet var totalTimeLabel: WKInterfaceLabel!
@IBOutlet var avgPaceLabel: WKInterfaceLabel!
@IBOutlet var table: WKInterfaceTable!
@IBAction func toBottomAction() {
    table.scrollToRowAtIndex(table.numberOfRows - 1)
}
@IBAction func toTopAction() {
    table.scrollToRowAtIndex(0)
}

Connect the controls to the outlets and actions. You’ll notice we included some code in the buttons. This will scroll to the top and bottom of the table using the scrollToRowAtIndex method. As we discussed in the previous lesson, we can place controls before and after the table as headers and footers. Close the assistant editor.

Adding a Model Class

We’ll use an array named data to hold our values. Add a file by pressing Command-N. Make a new Watchkit Class subclassing NSObject named RunData . When you save the class, be sure that you are saving in the extension group. When the code appears, add the following code:

class RunData: NSObject {
    var data = [654,862,860,802,774,716,892,775,748,886,835]
    var count:Int {
        get{
            return data.count
        }
    }
    //A function to change the seconds data from an integer to a string in the form 00:00:00
    class func paceSeconds(pace:Int) -> String{
        let hours = pace / 3600
        let remainingSeconds = pace % 3600
        let minutes = remainingSeconds / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours,minutes,seconds)

    }
        func totalTimeFromSplit(split:Int, toSplit:Int) -> Int{
        var total = 0
        for index in split...toSplit{
            total+=data[index]
        }
        return total
    }
    
    func avgPaceFromSplit(split:Int, toSplit:Int) -> Int{
        let count = ((toSplit) - split) + 1
        return totalTimeFromSplit(split, toSplit: toSplit) / count
    }

We done several things to this model. We added the array data. For ease in use, we added the property count. We added a conversion method from seconds to a string. We’ll find that this method paceSeconds is very useful as a class method, so we made it one. We also added two methods to calculate average paces and total times for a range of splits.

Make the Table

Go to the InterfaceController.swift code. Add your model just under the outlets.

 let runData = RunData()

Add the following method to the class

func tableRefresh(){
    table.setNumberOfRows(runData.count, withRowType: "row")
    for index in 0 ..< table.numberOfRows {
        let row = table.rowControllerAtIndex(index) as! TableRowController
        let rowString = String(format: "Split:%02i miles", index + 1)
        let paceString = "Pace:" + RunData.stringFromSeconds(runData.data[index])
        row.splits.setText(rowString)
        row.time.setText(paceString)
    }
    let totalPace = runData.totalTimeFromSplit(0, toSplit: runData.count - 1)
    let avgPace = runData.avgPaceFromSplit(0, toSplit: runData.count - 1)
    totalTimeLabel.setText(RunData.stringFromSeconds(totalPace))
    avgPaceLabel.setText(RunData.stringFromSeconds(avgPace))      
}

As discussed last lesson. WatchOS does away with all the delegates based table stuff. In its place is an array of table row controllers. We set the number of rows and the type of row in the table with setNumberOfRows. We loop through the array, assigning the current row to row and populate it with data. After the loop, we set the totals labels for the run. Add the method tableRefresh to viewWillAppear

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

Select a simulator of a 42mm watch. Build and run.

Photo Mar 29, 6 24 06 AM

Selecting Rows in a Table

Selecting row in a table is easy. You override the method table(table: didSelectRowAtIndex rowIndex:) method.
We’ll take our average pace and display it on a separate page when we select a row in the table. Add this selection method:

//table selection method
   override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        //build a context for the data
        
        let avgPace = runData.avgPaceFromSplit(0, toSplit:rowIndex)
        presentControllerWithName("Info", context: avgPace) //present the view controller
    }

Using the value of rowIndex, We made a string for our context. I first get the value I need from the appropriate function, then convert it to a string with paceSeconds. I assign the string to the context as an AnyObject. Finally I present the view controller with the name Info with the context.
Go to the InfoInterfaceController.Swift File. Change the class to this

class InfoInterfaceController: WKInterfaceController {

    @IBOutlet weak var paceLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        let pace = context as! Int
        paceLabel.setText(RunData.StringFromSeconds(pace))
    }
}

When the view awakes, we take the context, convert it back to a string and then place it in the label. Go back to the storyboard. Click the view controller icon on the info controller we just made. In the identity inspector make the controller InfoInterfaceController. Open the assistant editor, and drag from the circle in the view controller code to the white label until it highlights. Release the mouse button.

Build and run.

Click on the split for 5 miles,

Photo Mar 29, 6 24 06 AM

and you get an average pace for the split.

Photo Mar 29, 6 24 09 AM

Click Info to return to the table.

Adding a Context Menu

In the rest of the lesson, we’ll add a few new functions to control the table. This is a good place to use a context menu. Context menus will appear when you force touch on the watch. In the Object Library, find the context menu.

2016-03-29_06-33-56

On the storyboard, drag a menu from the object library to the table controller, and drop on top of the controller

2016-03-29_06-34-40

It disappears. You can only see it in the document outline. If the document outline is not open, open it by clicking the icon in the lower left corner of the storyboard.

2016-03-29_06-43-20

In the document outline, select the menu. In the attribute inspector, change the items to 3.

2016-03-29_06-35-30

Click on the arrow next to the menu. The document outline changes to have three menu Items.

2016-03-29_06-35-52

Menus can have both custom and built-in items. We’ll use built-in icons for this application. Click the top menu item in the document, In the attributes inspector, change the title to Add Row and change the image to Add.

2015-08-11_05-57-03

For the second menu item, change the title to Delete Row and the image to Trash. For the last item change the Title to Reset and the image to Repeat.

Open the assistant editor if not open. Set the editor to Automatic. From the document outline, control-drag from the menu items to the InterfaceController code. Make three actions addRow, deleteRow, and resetRows.

Close the assistant editor for now.

Build and run. On a watch, hold down on your watch. In the simulator,  you’ll need to change the pressure. From the drop down menu for the simulator, select Hardware>Force Touch Pressure>Deep Press or use the keyboard combination Shift-Command-2

2016-03-29_06-50-24

Now give a click on the watch face. The menu should appear:

Photo Mar 29, 6 55 00 AM

Set the simulator back to Shallow Press (Shift-Command-1) to select a button.

Adding Reset

We will be adding and deleting data in this demo. It will be helpful to add a reset method. Go to the RunData class. Add a class method like this.

//return the original array of Data
    class func resetData() -> [Int]{
        return [654,862,860,802,774,716,892,775,748,886,835]
    }

Since this is the same data in the array as the initialization, you can cut and paste that if you wish.

In InterfaceController, go to the menu action for the reset menu item. Change the code there to this:

    @IBAction func resetRows() {
        runData.data = RunData.resetData()
        tableRefresh()
        selectedRow = nil
    }

The code reloads the array into the data property. We then refresh the table.

Selecting the Row

We have an error on the last line of this method. As a property, add the following to the InterfaceController class:

var selectedRow:Int! = nil

We’ll need to keep track of the row we last selected. That will be the row we’ll use to add or delete rows. However we may have an unselected state. To keep track of this we use an optional value. If the property is nil, there is no selection. In resetRows we reset everything, so we lose our selection, and set selectedRow to nil
in our table:DidiSelectRowAtIndex method, add the following line as the first line of code in the method:

 selectedRow = rowIndex //for use with insert and delete

Adding An Alert

Whenever we select a row, we set our selectRow property.
since we can have a nil value, we need to handle trying to delete nothing. We’ll need an alert to tell the user this won’t work. Go to the storyboard. Add another interface. In the attributes inspector, make the identifier No Splits Alert and the title Back.

2015-08-11_06-59-51

Add two labels. Use an exclamation emoji at 56 point in the bottom one and The text No Splits were Selected. Set the width to Relative to Container and align it Centered. Add text to the label so the interface looks like this:

2016-03-29_13-37-55

I went lazy here and used the built-in back button to handle dismissing the alert. If you want you can make another WKInterfaceController class and add a button to dismiss the alert. We’ll come back to this shortly.

Deleting Rows From a Table

To delete a row, we delete the element in the array, then refresh the table. However, we also have to check for a nil value and handle those. Add this code to the deleteRow action in the Interface controller

    @IBAction func deleteRow() {
        if let row:Int = selectedRow{
            runData.removeItemAtIndex(row)
            tableRefresh()
            selectedRow = nil
        } else {
          presentControllerWithName("No Splits Alert", context: nil)
        }
    }

We use optional chaining to make row from selected row. If nil, we present the alert. Otherwise we run a method in the model to remove the item, refresh the table, and set selectedRow to nil. In RunData, we need to add that removeItemAtIndex method:

    func removeItemAtIndex(index:Int){
        data.removeAtIndex(index)
    }

Build and run. Go to the table, and then the menu. Hit Delete Row and we get our alert:

Photo Mar 31, 6 27 47 AM

Go back to the table and select the 9 mile.

Photo Mar 31, 6 28 25 AM

Go back to the table, and then delete from the menu. The 9 mile is still there but the pace changes to what the 10 mile pace was.

Photo Mar 31, 7 35 53 AM

Since we figured distance by the element number, our numbers mess up in this example. This is to keep things as simple as possible. If you wanted you could make a more robust model that had both miles and pace to prevent this problem.

Adding a Row to the Table

For adding we’ll do one of two things: if we select a row, we add that row at that index location. If we don’t select a row, we’ll add at the end of the splits, making a new finish time. But before we do we need to get what the pace is.

Adding Items  in the Model

We are trying to keep to MVC and all direct manipulations of the array happen in the model class. We add items to the array, so our model will need an add function. Add the following code to the RunData class:

func addItemAtIndex(index:Int,item:Int){
        data.insert(item, atIndex: index)
    }

Make a Input Controller

We are going to do a big cheat just to keep this short. We’ll use a text input and select a pace from the selections on the text input. In a real app this is not how to do this, but it demonstrates a text input and saves us from doing a lot of extra work building another view controller with delegates. The right way to do this is build another table with times in it, or to build a controller with three sliders, then use a delegate to get back your input to the InterfaceController class.

Text input controllers are a system generated controller that takes a string array of possible responses. You can set your controller to also select emoji and animated emoji. The user always has the option for using dictation, by pressing the microphone button that appears and returning the dictation as  text. It’s the microphone that’s the problem with this code. A good developer needs to handle the code for a dictation response. In our code we will only handle the selection response.

Go to InterfaceController.swift. In the addRow method add the two following declarations

 //selections for the text input
        let timeSelections = [
            "08:00 480 Seconds",
            "09:00 540 Seconds",
            "10:00 600 Seconds",
            "11:00 660 Seconds",
            "12:00 720 Seconds",
            "13:00 780 Seconds",
            "14:00 840 Seconds",
            "15:00 900 Seconds"]
        var seconds = 0

The timeSelections array contains information we’ll use to select our time.  We’ll use these as selections to display by getting sneaky later. We’ll use the variable seconds to store our final selection.  Add the text input controller under these declarations:

// Present a text controller with suggested times
presentTextInputControllerWithSuggestions(
    timeSelections,
    allowedInputMode: .Plain,
    completion: {(results) -> Void in
// Add the completion code here in the closure 
})

The method presentTextInputControllerWithSuggestions does all of our UI for us. We add our string array timeSelections and we have our selections displayed in a table view. The parameter  allowedInputMode has three choices.  The .Plain we selected will only allow text and no emoji.  The selections .AllowEmoji and .AllowAnimatedEmoji will do exactly as they say – Allow you to have standard emoji or return an animated graphic. We’ll stick to plain text. The last parameter is the closure completion: Add the completion handler in the closure

if results != nil && results!.count > 0 {
//selection made
    let aResult = results?[0] as! String
    let times = aResult.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
// we will make an assumption here and no one
// will use dictation. NOT a good idea
// but simplifies the code.
    seconds = Int(times[1])!
}

When the input controller completes, it returns [AnyObject]? into  results. This array will have only one element: our text string.  We extract that string into aResult. Then we get tricky. We turn that string into a string array, using the method componentsSeperatedByCharactersInSet.   I intentionally put a space between the time, seconds and word Seconds. Using the NSCharacterSet  which is all the white space characters, I break the text into a three element array. I know that the third element is the time in seconds, so I convert that to an integer.

We have the input. Add the time to the table:

if selectedRow != nil{
    runData.addItemAtIndex(selectedRow, item: seconds)
} else {
     runData.addItemAtIndex(runData.count - 1, item: seconds)
}
tableRefresh()

Make sure all of that code is inside the closure. The text input runs parallel to the table and wont update our table unless it is all in the closure.  We’ve set up everything. Build and run. Go to the menu and select Add Row. Scroll down and Select the time 12:00 720 seconds

Photo Mar 31, 7 38 19 AM

Tap the bottom button or scroll to the bottom. You will have a mile 12 with the finish data of 12:00.  The data appended to the table since we had no selected split.

Photo Mar 31, 7 41 21 AM

Select Mile 9, which has a 12:28 pace. We’ll find a 13:07 Average Pace,

Photo Mar 31, 6 28 30 AM (1)

Then exit from the average pace view and add a 8:00 pace.

Photo Mar 31, 6 28 55 AM (1)

Mile 9 is now 8:00 and mile 10 is 12:28. We inserted the new pace data into the selected slot.

Photo Mar 31, 7 45 36 AM (1)

Some of you have been itching to hit that microphone. If you have a watch to run the app on, go ahead and add another split. Tap the microphone and you get a voice input.

Photo Mar 31, 7 50 45 AM

Say ” Time eight three five seconds”

Photo Mar 31, 7 50 54 AM

Press Done. Time for mile 9 is now 13:55

Photo Mar 31, 7 51 04 AM

Try it again, this time saying “Time four thirty-five seconds”

Photo Mar 31, 7 51 46 AM

We crash since Int() returns nil. The words to/two and for/four confuse dictation.

You can add some protection like this.  Change the integer conversion to this:

// we will make an assumption here and no one
// will use dictation. NOT a good idea
// but simplifies the code.
// if we don't get an integer for the second element do nothing.
if let seconds = Int(times[1]){
    if self.selectedRow != nil{
        self.runData.addItemAtIndex(
             self.selectedRow,
               item: seconds)
    } else {
        self.runData.addItemAtIndex(
            self.runData.count - 1,
             item: seconds)
    }

Comment out the line

//var seconds = 0 

This will only add a row if we have valid data. there’s still several ways that data can be invalid,  such as an array less than two elements, but this solves a lot of them.

Subtotals and Multiple-Row Tables

We’ve looked at  the setNumberowRows:withRowType method to make a table with a single row type. For example we had this:

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

We used the count property of an array with our data to present and give one row type. For a multiple row types we use a different method:

table.setRowTypes(rowTypes)

where rowtypes is an array of strings identifying row types. Row types are the row identifiers in the storyboard for row controllers in a table. I could, for example, have a table set like this in my storyboard:

2016-03-31_08-01-22

I would take each of the groups in this table and identify them as a part of the table: Header, Sub Total, Row, and Total.
If I had 4 data points like this:

  var data = [740,745,750,740]

I could make a row header like this to show the correct type of row by the an element in row type.

var rowTypes = ["Header","Sub Total", "Row","Row","Sub Total", "Row","Row","Total"]

Swift would know how many rows we have here since it is rowTypes.count. If you are sharp and have followed along, you’ll notice the problem there is with this code. In our previous lessons, we would get a row with a loop like this:

 
for var index = 0; index < table.numberOfRows; index++ { 
    let row = table.rowControllerAtIndex(index) as! TableRowController //get the row
    let dataString = String(format:"%02",data[index])
    //Set the properties of the row Controller.
    row.label.setText(dataString)
} //end loop

We get a row, and downcast it to the correct row controller class. We take the data in the array, index to place the data in the row controller’s label. This works fine with a single row type table. Add more than one row type and things get a bit dicey. We don’t know what class or row controller to use, and we don’t know where the data is since there is not a one to one correspondence with the index. In the rowTypes array above, the value of data[0] is in rowtype[2]

Some people will only use a header and a footer.

var rowTypes = ["Header","Row","Row","Row","Row","Footer"]

For those cases you could put some code before and after the loop to handle the header and footer.  A better option is not use a header or footer, and make the header and footer information outside the table like was have our totals in our demo. Subheads and sub footers are not so easy though. How do you know when they exist, and what do you do when they show up in the array? They’ll also mess up correspondence to the data array even more than before.

For all these cases it’s easier to have a switch..case in our loop to parse the type. Read the rowtype array, and then react accordingly.

So far I’ve kept these two broken into two arrays. We could use one array with a class, struct or dictionary. We’ll use the dictionary [String:AnyObject] and have two keys: time and type. We’ll generate an array before we display it with the correct types.

Many runners like to break their runs into 5 kilometer chunks. While not exactly 5 Kilometers(3.11 miles), we can get a close estimate by using 3 miles. Every three miles we’ll add another row with the average pace for the three rows before it.

Change the Storyboard

Go to the storyboard in the App Extension. Select the table. In the attributes inspector, change the number of Prototype Rows from 1 to 2,

2016-03-31_08-05-37

In the document outline, you will see the two row controllers:

2016-03-31_08-08-53

We have one Row and one Table Row Controller. Click on the Table Row Controller in the document outline. In the attribute inspector, change the Identifier for the row controller to Split5K. Also deselect the Selectable check box.

2016-04-01_06-05-10

It will change in the document outline to Split5K.  Select the group in the table controller. Change the height to Size to Fit ContentChange the background color to  Green(#00CC00).

Drag  to the Split5K group on the storyboard a label. Change the width to Relative to Container.   Change the text on the top label to 5K – . Change the font to Subhead, and the text color to Black(#000000). When done, your table should look like this.

2016-03-31_08-20-47

Add TableRowControllers

We need a class for the new row controllers. On the drop-down menu, go to File>New>File or Tap Command-N on the keyboard. Make a new WatchOS class Split5kTableRowController subclassing NSObject. Save the file in the WatchKit extension group.

Go back to the storyboard. In the document outline select the Split5K table row controller. In the identity inspector, change the class to Split5KTableRowController. Open the assistant editor. In the assistant editor you will have to manually specify the Split5KTableRowController.

If not visible, open the outline for the Split5K row so you can see the label. This is one of those times it is much easier to select from the outline than the objects on the storyboard. Control drag the label to the row controller class. Make a label outlet named splits.

Close up the assistant editor.

Coding the Multiple Row View Controller

With a multiple row controllers, the data in the array the table does  not have a 1:1 index relationship since the 5k splits get in the way of the index count for the data.  We solve that by making several arrays which will have a common index. Add this to our code’s  property declarations

var tableTime = [Int]()
var tableRowType = [String]()
var tableSplit = [Int]()

These three arrays hold the data for the table, in the table’s order.  tableTime is the time in seconds. We keep track of the type  in the array tableRowType.  Our last array tableSplit does double duty.  For a row, it has the index of the data array. For a 5K split, it has the distance in kilometers.

The refreshTableData() method

Before we display a table, we’ll construct these three arrays with new data.  Add the following method and code to your InterfaceController class:

func refreshTableData(){
    var rowType = "row"
    var rowTime = 0
    var rowSplit = 0
    let subtotalEntry = 3
    var index = 0
    var counter = 0
    var counter5K = 1
    
// clear  the arrays
        tableTime = []
        tableRowType = []
        tableSplit = []

The variable rowType, rowTime,and rowSplit hold the values we’ll add to the array for each row in the table. Or constant subtotalEntry indicates that every three rows we’ll add a 5K Split row.  We’ll use several counters index, counter, and counter5k to keep track of all the counts and indices. We are going to use a while loop to construct our table, and thus the need for counters.  Add this to the code:

 
//populate the arrays
while index < runData.count{
//every three rows, add a 5k pace row
    if counter == subtotalEntry{
        counter = 0 //reset counting 3 rows
        counter5K += 1
//Add a regular row
            } else {
                index += 1
                counter += 1
            }

The variable counter counts to 3. When it reaches 3, we include a 5K split row, then reset counter. In that row we’ll use the current index to figure out the intervals to average.  To keep track of the distance measure for the 5k splits, we have another counter counter5K. For data rows, we grab the current index and  access the correct element in  runData.data, then increment our counters. Flesh this out by assigning rowType, rowTime,and rowSplit proper values:

//populate the arrays
while index < runData.count{
//every three rows, add a 5k pace row
    if counter == subtotalEntry{
        counter = 0 //reset counting 3 rows
        rowType = "Split5K"
        rowTime = runData.avgPaceFromSplit(
            (index - (subtotalEntry - 1)),
            toSplit: index)
        rowSplit = counter5K * 5
        counter5K += 1
//Add a regular row
    } else {
        rowType = "row"
        rowTime = runData.data[index]
        rowSplit = index
        index += 1
        counter += 1
    }

The variable rowType sets to one of our two controllers: row or Split5k.  We get our data and the index in rowTime and rowSplit respectively. Now add this inside the loop, under the if..else clause:

 //Add the data to the array
            tableTime += [rowTime]
            tableRowType += [rowType]
            tableSplit += [rowSplit]

This appends the data to the three arrays. We end up with three arrays usable by our table.

Modifying the tableRefresh Method

With good data we can modify the tableRefresh method in InterfaceController. In the code so far we had the table.setNumberOfRows method. Comment that out and replace it like this:

//refresh the data
refreshTableData()
//set the rows in the table
//table.setNumberOfRows(
//    runData.count,
//     withRowType: "row")
table.setRowTypes(tableRowType)

Instead of setting the number of rows, we give the table a string array with the table types. When using setRowTypes, I strongly suggest naming everything the same. Your storyboard identifier should be the same as your view controller name and the type name you use here. I used Split5k for all three. The compiler looks for a controller Split5KTableRowController when you have a type of Split5K.  If you are getting nil errors when accessing a table row, this may be the culprit.

We have to deal with both view controllers now. In many cases, it may be more, so I tend to use a switch statement to parse between the different controllers.  Change the for loop to this

for index in 0 ..< table.numberOfRows {
    switch tableRowType[index]{
    case "Split5K":
        let row = 
            table.rowControllerAtIndex(index) 
            as! Split5KTableRowController
        let paceString = 
           String(
               format: "%iK - ",
               tableSplit[index]) + 
           RunData.stringFromSeconds(
               tableTime[index]) 
           + " Pace"
          row.splits.setText(paceString)
    default:
        let row = 
            table.rowControllerAtIndex(index) 
                 as! TableRowController
        let rowString = String(
            format: "Split:%02i miles",
            tableSplit[index] + 1)
        let paceString = "Pace:" +
              RunData.stringFromSeconds(
                   tableTime[index])
         row.splits.setText(rowString)
         row.time.setText(paceString)
      }
}

In our earlier iteration, numberOfRows was set explicitly. In this version,  it is implicit as the size for the tableRowType array sets our size.  In the loop we get tableRowType[index] to parse the row type. For 5K Splits we have one code. In this example, I set us up to turn everything into a row that wasn’t anything else.  You could make a row case and an default case for an error, but I was trying to be compact here.

Notice we changed the code in the original version for the rows  to use the arrays, not index and runData.data

let rowString = String(
    format: "Split:%02i miles", 
    tableSplit[index] + 1)
let paceString = "Pace:" +
    RunData.stringFromSeconds(tableTime[index])

Similarly, we used the arrays in the 5K split code

let row = table.rowControllerAtIndex(index) 
    as! Split5KTableRowController
let paceString = String(format: "%iK - ", 
             tableSplit[index]) + 
     RunData.stringFromSeconds(tableTime[index])
     + " Pace"

 

Build and run.  The splits show up.

Photo Apr 01, 9 38 49 AM

But if you try to select a row, the app crashes.

Selection with Multiple Rows

Why did it fail?  Look at the  didSelectRowAtIndex code.


override func table(
    table: WKInterfaceTable,
    didSelectRowAtIndex rowIndex: Int) 
{
     selectedRow = rowIndex 
let avgPace = runData.avgPaceFromSplit(0, 
     toSplit:rowIndex)
presentControllerWithName("Info", 
     context: avgPace) //present the viewcontroller
}

row data mapping 1We access data from the rowIndex. In a single row table, this is  no problem.  However with a multi-Row table the index does not describe the data. Look at the illustration above. With our code, rowIndex 6 would try to get data from dataIndex 6, which causes an overflow error.

On the other hand, if we have a list somewhere that says row 2 is really row 0 , we are row data mapping pointer 1all set. That’s why TableSplit isn’t  cosmetic, but critical. For rows, it holds the index back to  runData.data.  We need to make only a few changes to get the selection code to work.

Change

selectionIndex = rowIndex

 to this:

selectionIndex = tableSplit[rowIndex]

With this small change, all our add and delete functions will work right too, since they depend on selectionIndex to work with the correct rows.

We also will need a change for the context. Change this

let avgPace = runData.avgPaceFromSplit(0, 
     toSplit:rowIndex)

to this:


let avgPace = runData.avgPaceFromSplit(0,
     toSplit:rowIndex)

Build and run. You can now select a row.

Photo Mar 29, 6 24 09 AM

With this code you can get rather sophisticated with table views in a watch. The question remains if you would want to. A watch is to be looked at for a seconds, not minutes, hence long tables are probably a bad idea. However, if your application calls for them, they are an option.

The Whole Code

Interfacecontroller.swift

//
//  InterfaceController.swift
//  SwiftWatchOSTableDemo WatchKit Extension
//
//  Created by Steven Lipton on 3/24/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {

    @IBOutlet var totalTimeLabel: WKInterfaceLabel!
    @IBOutlet var avgPaceLabel: WKInterfaceLabel!
    @IBOutlet var table: WKInterfaceTable!
    let runData = RunData()
    var selectedRow:Int! = nil
    var tableTime = [Int]()
    var tableRowType = [String]()
    var tableSplit = [Int]()
    
    @IBAction func addRow() {
        //selections for the text input
        let timeSelections = [
            "08:00 480 Seconds",
            "09:00 540 Seconds",
            "10:00 600 Seconds",
            "11:00 660 Seconds",
            "12:00 720 Seconds",
            "13:00 780 Seconds",
            "14:00 840 Seconds",
            "15:00 900 Seconds"]
        // Present a text controller with suggested times
        presentTextInputControllerWithSuggestions(timeSelections,
            allowedInputMode: .Plain,
            completion: {(results) -> Void in
                if results != nil && results!.count > 0 {
                    //Selection made
                    let aResult = results?[0] as! String
                    let times = aResult.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
                    // We will make an assumption here and no one
                    // Will use dictation. NOT a good idea
                    // but simplifies the code.
                    // If we dont get an integer for the second element do nothing.
                    if let seconds = Int(times[1]){
                        if self.selectedRow != nil{
                            self.runData.addItemAtIndex(self.selectedRow, item: seconds)
                        } else {
                            self.runData.addItemAtIndex(self.runData.count, item: seconds)
                        }
                        self.tableRefresh()
                    }
                }
        })
    }
    
    @IBAction func deleteRow() {
        if let row:Int = selectedRow{
            runData.removeItemAtIndex(row)
            tableRefresh()
            selectedRow = nil
        } else {
            presentControllerWithName("No Splits Alert", context: nil)
        }
    }
    
    @IBAction func resetRows() {
        runData.data = RunData.resetData()
        tableRefresh()
        selectedRow = nil
    }
    @IBAction func toBottomAction() {
        table.scrollToRowAtIndex(table.numberOfRows - 1)
    }
    @IBAction func toTopAction() {
        table.scrollToRowAtIndex(0)
    }
    
    func refreshTableData(){
        var rowType = "row"
        var rowTime = 0
        var rowSplit = 0
        
        let subtotalEntry = 3
        var index = 0
        var counter = 0
        var counter5K = 1
        // clear  the arrays
        tableTime = []
        tableRowType = []
        tableSplit = []
        
//Populate the arrays
       while index < runData.count{
//Every three rows, add a 5k pace row
            if counter == subtotalEntry{
                counter = 0 //reset counting 3 rows
                rowType = "Split5K"
                rowTime = runData.avgPaceFromSplit( (index - (subtotalEntry - 1)), toSplit: index)
                rowSplit = counter5K * 5
                counter5K += 1
//Add a regular row
            } else {
                rowType = "row"
                rowTime = runData.data[index]
                rowSplit = index
                index += 1
                counter += 1
            }
//Add the data to the array
            tableTime += [rowTime]
            tableRowType += [rowType]
            tableSplit += [rowSplit]
        }
        
}
    
    func tableRefresh(){
        //Refresh the data
        refreshTableData()
        //Set the rows inthe table
        //table.setNumberOfRows(runData.count, withRowType: "row")
        table.setRowTypes(tableRowType)
        for index in 0 ..< table.numberOfRows {
            switch tableRowType[index]{
            case "Split5K":
                let row = table.rowControllerAtIndex(index) as! Split5KTableRowController
                let paceString = String(format: "%iK - ", tableSplit[index]) + RunData.stringFromSeconds(tableTime[index]) + " Pace"
                row.splits.setText(paceString)
            default:
                let row = table.rowControllerAtIndex(index) as! TableRowController
                let rowString = String(format: "Split:%02i miles", tableSplit[index] + 1)
                let paceString = "Pace:" + RunData.stringFromSeconds(tableTime[index])
                row.splits.setText(rowString)
                row.time.setText(paceString)
            }
        }
        let totalPace = runData.totalTimeFromSplit(0, toSplit: runData.count - 1)
        let avgPace = runData.avgPaceFromSplit(0, toSplit: runData.count - 1)
        totalTimeLabel.setText(RunData.stringFromSeconds(totalPace))
        avgPaceLabel.setText(RunData.stringFromSeconds(avgPace))
    }
    //Table selection method
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        //for use with insert and delete
        selectedRow = tableSplit[rowIndex]
        
        //Build a context for the data
        let avgPace = runData.avgPaceFromSplit(0, toSplit:selectedRow)
        presentControllerWithName("Info", context: avgPace) //present the viewcontroller
           }
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
    }

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

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

}

RunData.swift

//
//  RunData.swift
//  SwiftWatchOSTableDemo
//
//  Created by Steven Lipton on 3/24/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class RunData: NSObject {
    var data = [654,862,860,802,774,716,892,775,748,886,835]
    var count:Int {
        get{
            return data.count
        }
    }
    //A function to change the seconds data from an integer to a string in the form 00:00:00
    class func stringFromSeconds(pace:Int) -> String{
        let hours = pace / 3600
        let remainingSeconds = pace % 3600
        let minutes = remainingSeconds / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours,minutes,seconds)
    }
    //Reset the original array
    class func resetData() -> [Int]{
        return [654,862,860,802,774,716,892,775,748,886,835]
    }
    //Remove a data item
    func removeItemAtIndex(index:Int){
        data.removeAtIndex(index)
    }
    //Add a data item
    func addItemAtIndex(index:Int,item:Int){
        print ("inserting \(item) at index \(index)")
        data.insert(item, atIndex: index)
    }
    
    func totalTimeFromSplit(split:Int, toSplit:Int) -> Int{
        var total = 0
        for index in split...toSplit{
            total+=data[index]
        }
        return total
    }
    
    func avgPaceFromSplit(split:Int, toSplit:Int) -> Int{
        let count = ((toSplit) - split) + 1
        return totalTimeFromSplit(split, toSplit: toSplit) / count
    }
}

InfoInterfaceController.swift

//
//  InfoInterfaceController.swift
//  SwiftWatchOSTableDemo
//
//  Created by Steven Lipton on 3/24/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InfoInterfaceController: WKInterfaceController {
 @IBOutlet var paceLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        
        super.awakeWithContext(context)
        let pace = context as! Int
        paceLabel.setText(RunData.stringFromSeconds(pace))
    }

}

TableRowController.swift

//
//  TableRowController.swift
//  SwiftWatchOSTableDemo
//
//  Created by Steven Lipton on 3/24/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit

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

Split5KTableRowController.swift

//
//  Split5KTableRowController.swift
//  SwiftWatchOSTableDemo
//
//  Created by Steven Lipton on 3/31/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit

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

Swift WatchKit: Selecting With Multiple Rows in Apple Watch

row data mapping illustration

In the last lesson we created a multi-row table. However, we can only view the table, not select from the table. Multi-row tables provide some challenges with selection. Along the way, we’ll make a new interface to display selected information, using a technique we have not yet covered in this series: A dictionary as a context.

The problem of a muti row table is rather annoying. We have an array with different types of data stored in it for the different row types, all mixed together. We have header, footer,  row, and subhead data. Each uses the same data type but uses them differently. Subheads summarize what a mile or running looks like, while rows give us the data for that interval.

Basic Selection

We are going to modify the project from the last lesson to add selection. Go to the storyboard. In the document outline select the Header row. In the attribute inspector, note the Allow Selection Checkbox is unchecked

2015-08-26_06-31-28

Now select the Row table controller. The Allow Selection Checkbox is checked.

2015-08-26_06-33-49

As the title states, the rows checked for selection will allow selection. We checked for the subheader and the row, and unchecked for the header and footer.

In our InterfaceController code add the following

 override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
	// code goes here
}

This is the basic selection function. row data mapping 1We access data from the rowIndex. In a single row table, this is easy. However with a multi-Row table the index does not describe the data, so we need to do a little work to get the data we are interested in. We’ll look at three ways to do this.

A New Interface

Before we do anything we need another interface to display our information. Go to the storyboard. Drag a new interface controller to the storyboard. Add three labels and a button. Once added, position the button to the bottom and the three labels remain on the top. Title the button Done. Title the labels Distance, Pace and Title like this:

2015-08-26_06-30-47

Press Command-N and make a new InterfaceController subclass called InfoInterfaceController. Make sure to save it in the Extension group. Leave it blank for now and go back to the storyboard. Select the our view controller’s icon and in the attributes inspector, set the identity and title to Info. In the Identity inspector set the class to InfoInterfaceController

Open the assistant editor set to Automatic. Control-drag from each label to the code. Make outlets named pace, distance and infoTitle to the corresponding label. Control-drag from the button to the code and make an action named doneViewing. Your code should now have these lines.

    @IBOutlet var pace: WKInterfaceLabel!
    @IBOutlet var distance: WKInterfaceLabel!
    @IBOutlet var infoTitle:WKInterfaceLabel!
    
    @IBAction func doneViewing() {
    }

Change the action to this:

@IBAction func doneViewing() {
        dismissController()
    }

We now have a big dismissal button for the modal interface we created.

A Dictionary Context

Close the assistant editor and go to the InfoInterfaceController.swift file. We’ll only need awakeWithContext so you can delete the other life cycle methods if you wish. In awakeWithContext code this:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    //context is sent as a Dictionary of [String:String]
	let data = context as! [String:String]
}

In previous lessons we used a class or a struct for a context. This time we’ll use a dictionary with string values. We convert the AnyObject! context to the dictionary [String:String] a dictionary with strings for keys and strings for values. We picked strings for a reason: they can be directly displayed by the setText method of WKInterfaceLabel. Add this at the bottom of awakeWithContext

	distance.setText( "Distance:" + data["Distance"]!)
	pace.setText("Pace:" + data["Pace"]!)
	infoTitle.setText(data["Title"])

When we call the modal, it will assign the correct strings to the labels. Note here I did this the fast simple way. In production code, you should be checking for nil values.

Setting up the Selection Method

Switch over to the InterfaceController.swift file. Change the table:didSelectRowAtIndex: to this:

    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        //selection of data and presenting it to
        var myDistance = "0 miles"
        var myPace = "00:00:00"
        var myTitle = "No title yet"
        var context = [String:String]()
        //code goes here	
        presentControllerWithName("Info", context: context)
    }

We’ve made a few variables to work with, and set up the format of our context as a dictionary. Our last line presents this dictionary to the modal Info. In between, we’ll set up a switch statement to access the data we need for the modal. Add this above the presentControllerWithName line:

switch rowTypes[rowIndex]{
	case "Row":            
		myTitle = "0.5 mile data"
		context = ["Pace":myPace,"Distance":myDistance,"Title":myTitle]
	case "Subhead":
		myTitle = "1.0 mile data"
		context = ["Distance":myDistance,"Pace":myPace,"Title":myTitle]
	default:
		myTitle = "Not a value row type for selection: " + rowTypes[rowIndex]
		print(myTitle)
		context = ["Distance":"Error","Pace":"Error","Title":myTitle]
}  

This time we only need the two rows we set up for selection. We can skip Header and Footer since it will never run this code.

Selection Option 1: A Pointer to the Data Array

row data mapping pointer 1How do you access data in data[0] when you are accessing rowdata[2]? The index of the data set, our rows is not the same index as the rows in our table. The first answer is to include a pointer to the index. We set that up in the last lesson in the class RowData:

class RowData {
    var data:Int = 0
    var index:Int = 0
}

The last lesson had a method addRowWithType to add rows to the table with RowData. While we had the index for the data handy, we saved it.

func addRowWithType(type:String,withData data:Int, withIndex index:Int){
        rowTypes = rowTypes + [type] //append the rowtype array
        let newData = RowData()
        newData.data = data
        newData.index = index
        rowData = rowData + [newData] //append the rowData array
    }

While we also stored the data itself (we’ll get to that shortly) we stored the index  of the  array data  that held the data. We are using a simple type Int for the data. If we used a custom class in the  data instead, this way would point us to the array and we could access all the properties and methods for the class with a simple pointer.

In our example, we will grab the data from the data array and compute the distance based that index pointer. Add this just under the myTitle = "0.5 mile data" line

//using a pointer to the original data
let dataIndex = rowData[rowIndex].index
myDistance = String (format:"%02.1f",(Double(dataIndex + 1) / splitsPerMile ))
myPace = paceSeconds( data[dataIndex])

We make a constant dataIndex for the pointer to the original data. We can get the distance by dividing by the number of splits. We need a constant for that so make a class constant at the top of the class:

let splitsPerMile = 2.0

Pace is our data, which we can get from data[dataIndex]. This option is a very good one, with a low memory impact for bigger tables.

Build and run. Select the 12:20  row, and you get this:

2015-08-26_07-51-17

Selection Option 2: Add to the Model

row data rowData arrayOur second option we’ve also implemented: using the rowData array to store our values for us. Currently it stores the pace only, but with a few lines of code, it can store the distance as well. Change the class definition of RowData to this:

class RowData {
    var data:Int = 0
    var index:Int = 0
    var distance = 0.0
}

We added a Double named Distance as a property. Now change addRowWithType to add a calculation for distance:

func addRowWithType(type:String,withData data:Int, withIndex index:Int){
	rowTypes = rowTypes + [type] //append the rowtype array
	let newData = RowData()
	newData.data = data
	newData.index = index
	newData.distance = Double(index + 1) / splitsPerMile // <----added line
	rowData = rowData + [newData] //append the rowData array

Back in the table:didSelectRowAtIndex: method, Comment out this code

/*          let dataIndex = rowData[rowIndex].index
            myDistance = String (format:"%02.1f",(Double(dataIndex + 1) / splitsPerMile))
            myPace = paceSeconds( data[dataIndex])
*/

Add this code under the commented out code

//Using the data model
            myPace = paceSeconds(rowData[rowIndex].data)
            myDistance = String (format:"%02.1i",rowData[rowIndex].distance)

Build and run and you will get the same results as before.

2015-08-26_07-51-17

This option keeps everything handy in the array rowData[rowIndex], which we computed when we built the table. No tracing of where something is really stored is necessary.

Selection Option 3: Add to the Row Controller Class

row data mapping classThe last option might be the most controversial, and more complicated to set up . Instead of storing the data in a property of the InterfaceController for the table, store the data in the row controller. Like the second option, it has the advantage of having everything right where we need it. One could argue it breaks MVC however. You could argue that the model should stay the model of the Interface controller, and the row controller should be thought of more like views than controllers. You could also argue that the table row controller is a controller, and as such should have its own model.

I would probably use this option when my view controller model was generally the same for all rows, but I had a lot of processing to do for a  row type. In our example, let’s make the myDistance a computed property, and when we set the pace, update the label.

class SubheadTableRowController: NSObject {
    var myPace = 0
    var myPacestring:String = "" {
        didSet {
            label.setText(myPacestring)
        }
    }
    var numberOfSplits = 0.0
    var splitsPerMile = 0.0
    var myDistance:Double{
        return numberOfSplits / splitsPerMile
    }
   @IBOutlet weak var label:WKInterfaceLabel!
}

We added several properties, including a computed property and a property observer. We could add some methods as well, depending on the situation. If there were methods to update the label, based on the properties, then this is a way to go.

When we display the row, in refreshBuildtable, we would need to update the properties along with the display. Change the case "Subhead" in the  SubheadTableRow controller from this:

case "Subhead":
    let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
    row.label.setText("Avg Pace: " + paceSeconds(rowData[rowIndex].data))

to this:

case "Subhead":
    let row = table.rowControllerAtIndex(rowIndex)
        as!  SubheadTableRowController
    row.myPace = rowData[rowIndex].data
    row.myPacestring = paceSeconds(row.myPace)
    row.numberOfSplits = Double(rowData[rowIndex].index + 1)
    row.splitsPerMile = splitsPerMile

We set the properties and the controller did the rest. Interestingly, we did use our option 2 data to set this up in the controller. In the selection method, we now access everything from the row controller. Add this code to our table:didSelectRowAtIndex:  method

case "Subhead":
	myTitle = "1.0 mile data"
	//example of storing data in the row controller
	let row = table.rowControllerAtIndex(rowIndex) as! SubheadTableRowController
	myPace = row.myPacestring
	context = ["Distance":myDistance,"Pace":myPace,"Title":myTitle]

We get the row from the table, then access the properties. At the selection part, using a class simple way. Build and run

Select the first  subheading, and you get a result

2015-08-26_08-42-36

These three are not exclusive. We used option two to get our option three for example, and we very easily could have used all three. Each has its benefits. Option one points to a model well, separating the model from the controller. If we use a class instead of a type for the array value, we can store and process that data rather nicely. Both option two and option one use a second array from our data array, one we need to display our data. Option three give us greater control at the Row controller level,  when we need to do a lot of processing for the final output. In reality, you would use each option as a tool to get to the a final result.

The Whole Code

//
//  InterfaceController.swift
//  WatchKitMultiRow WatchKit 1 Extension
//
//  Created by Steven Lipton on 8/17/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

class RowData {
    var data:Int = 0
    var index:Int = 0
    var distance = 0.0
}

class InterfaceController: WKInterfaceController {
    
    @IBOutlet weak var table: WKInterfaceTable!
    let splitsPerMile = 2.0
    var rowData:[RowData] = []
    var rowTypes = ["Header","Subhead", "Row","Row","Subhead", "Row","Row","Footer"]
    var data = [740,745,750,740]
    //var data = [740,745,750,740,760,765,770,755]
    func avgData(array:[Int],start:Int,end:Int) -> Int{
        var total = 0
        for index in start...end{
            total = total + array[index]
        }
        return total / (end - start + 1)
    }
    
    func paceSeconds(pace:Int) -> String{
        let hours = pace / 3600
        let minutes = (pace - (hours * 3600 )) / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours, minutes,seconds)
    }
    func refreshTable(){
        var dataIndex = 0
        table.setRowTypes(rowTypes)
        for var rowIndex = 0; rowIndex > rowTypes.count; rowIndex++ {
            switch rowTypes[rowIndex]{
            case "Header":
                let row = table.rowControllerAtIndex(rowIndex) as! HeaderTableRowController
                row.label.setText(String(format:"Count: %i",data.count))
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                let avg = paceSeconds(avgData(data, start: 0, end: dataIndex))
                row.label.setText("Avg Pace: " + avg)
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText("Pace " + paceSeconds(data[dataIndex++]))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                let avg = paceSeconds(avgData(data, start: 0, end: data.count - 1))
                row.label.setText("Pace: " + avg)
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }
    //MARK: Iteration 2 of the table code
    func addRowWithType(type:String,withData data:Int, withIndex index:Int){
        rowTypes = rowTypes + [type] //append the rowtype array
        let newData = RowData()
        newData.data = data
        newData.index = index
        newData.distance = Double(index + 1 ) / splitsPerMile // <----added line rowData = rowData + [newData] //append the rowData array } func buildTable(){ //clear the arrays rowTypes = [] rowData = [] //make counters var dataIndex = 0 var subheadIndex = 1 // skipping zero //make a header addRowWithType("Header", withData: data.count, withIndex: 0) //loop through the data let subHeadInterval = 2 //if we are on an even row except 0, add a subhead/foot for var index = 0; index > data.count; index++ {
            if index % subHeadInterval == 0 && index != 0{
                addRowWithType("Subhead", withData: avgData(data, start: index - subHeadInterval, end: index - 1), withIndex: subheadIndex++)
            }
            // add the row data
            addRowWithType("Row", withData: data[index], withIndex: dataIndex++)
            
        }
        //add the footer
        addRowWithType("Footer", withData: avgData(data, start: 0, end: data.count - 1), withIndex: 0)
    }
    
    func refreshBuildtable(){
        buildTable() //refresh the table data
        table.setRowTypes(rowTypes) //set the row types
        //loop through the rowtype table
        for var rowIndex = 0; rowIndex > rowTypes.count; rowIndex++ {
            //parse the rowtypes
            switch rowTypes[rowIndex]{
            case "Header":
                let row = table.rowControllerAtIndex(rowIndex) as! HeaderTableRowController
                row.label.setText(String(format:"Count: %i",rowData[rowIndex].data))
                
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                row.myPace = rowData[rowIndex].data
                row.myPacestring = paceSeconds(row.myPace)
                row.numberOfSplits = Double(rowData[rowIndex].index + 1)
                row.splitsPerMile = splitsPerMile
                
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText(paceSeconds(rowData[rowIndex].data))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                row.label.setText("Pace: " + paceSeconds(rowData[rowIndex].data))
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }
    
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        //selection of data and presenting it to
        var myDistance = "0 Miles"
        var myPace = "00:00:00"
        var myTitle = "No title yet"
        var context = [String:String]()
        switch rowTypes[rowIndex]{
        case "Row":
            
            myTitle = "0.5 mile data"
            //using a pointer to the original data
/*            let dataIndex = rowData[rowIndex].index
            myDistance = String (format:"%02.1f",(Double(dataIndex + 1) / splitsPerMile))
            myPace = paceSeconds( data[dataIndex])
*/
            
            //Using the data model
            myPace = paceSeconds(rowData[rowIndex].data)
            myDistance = String (format:"%02.1i",rowData[rowIndex].distance)
            context = ["Pace":myPace,"Distance":myDistance, "Title":myTitle]

        case "Subhead":
            myTitle = "1.0 mile data"
            //example of storing data in the row controller
            let row = table.rowControllerAtIndex(rowIndex) as! SubheadTableRowController
            myPace = row.myPacestring
            myDistance = String(format: "%02.1f miles", row.myDistance)
            context = ["Distance":myDistance,"Pace":myPace,"Title":myTitle]
        default:
            myTitle = "Not a value row type for selection: " + rowTypes[rowIndex]
            print(myTitle)
            context = ["Distance":"Error","Pace":"Error","Title":myTitle]
        }
        presentControllerWithName("Info", context: context)
    }
    
    //MARK: life cycle
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        //refreshTable()
        refreshBuildtable()
    }
}
//
//  InfoInterfaceController.swift
//  WatchKitMultiRow
//
//  Created by Steven Lipton on 8/22/15.
//  Copyright © 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InfoInterfaceController: WKInterfaceController {
    
    @IBOutlet var pace: WKInterfaceLabel!
    @IBOutlet var distance: WKInterfaceLabel!
    @IBOutlet var infoTitle:WKInterfaceLabel!
    
    @IBAction func doneViewing() {
        dismissController()
    }
    override func awakeWithContext(context: AnyObject?) {
        //context is sent as a Dictionary of [String:String]
        super.awakeWithContext(context)
        // Configure interface objects here.
        let data = context as! [String:String]
        distance.setText( "Distance:" + data["Distance"]!)
        pace.setText("Pace:" + data["Pace"]!)
        infoTitle.setText(data["Title"])
    }
}
//
//  SubheadTableRowController.swift
//  wkMultirowSelect
//
//  Created by Steven Lipton on 8/24/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class SubheadTableRowController: NSObject {
    var myPace = 0
    var myPacestring:String = "" {
        didSet {
            label.setText(myPacestring)
        }
    }
    var numberOfSplits = 0.0
    var splitsPerMile = 0.0
    var myDistance:Double{
        return numberOfSplits / splitsPerMile
    }
   @IBOutlet weak var label:WKInterfaceLabel!
}

//
//  HeaderTableRowController.swift
//  wkMultirowSelect
//
//  Created by Steven Lipton on 8/24/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class HeaderTableRowController: NSObject {
    @IBOutlet weak var label:WKInterfaceLabel!
}

//
//  HeaderTableRowController.swift
//  wkMultirowSelect
//
//  Created by Steven Lipton on 8/24/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class HeaderTableRowController: NSObject {
    @IBOutlet weak var label:WKInterfaceLabel!
}

//
//  RowTableRowController.swift
//  wkMultirowSelect
//
//  Created by Steven Lipton on 8/24/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class RowTableRowController: NSObject {
    @IBOutlet weak var label:WKInterfaceLabel!
}

//
//  FooterTableRowController.swift
//  wkMultirowSelect
//
//  Created by Steven Lipton on 8/24/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class FooterTableRowController: NSObject {
    @IBOutlet weak var label:WKInterfaceLabel!
}

Swift WatchKit: Selecting, Deleting and Adding Rows in an Apple Watch Table

In our first part of this series, we made a simple dynamic table for the Apple Watch. Based on some pace data when I ran the Hot Chocolate 15K, we displayed the pace I ran at the mile splits. In a real running app, I would not want to add or delete any of my splits. However, many table-based apps do need deletion and addition. In this part, we’ll add methods for deletions and additions to a table. We’ll also use the methods for selection of a row in a table, and expand our simple array to a full model class.

We’ll be using the code we added in the last lesson to code this lesson. Be aware we will use menus and modal controllers in this lesson, so if you are not yet familiar on how those work, take a look at the lesson on context menus and on programmatic modal controllers. We’ll also be using class methods, so if you need a refresher on that, you can try here.

Adding a Model Class

In the previous lesson, we used an array named data to hold our values. We are going to continue with that array, but we will be adding a lot of methods dealing directly with this array. It makes a lot of sense to make a  model class with this array before we get working on modifying the table.

Open the project if not already open. If you are reading this first, go back to the previous lesson and follow directions there tp get the project working.  Once your project is open, add a file by pressing Command-N. Make a new Cocoa Touch class subclassing NSObject named RunData . When you save the class, be sure that you are saving in to the extension group.

2015-08-11_05-43-45

Once loaded, go back to the InterfaceController.Swift file. Cut and paste these lines of code from InterfaceController to the RunData class:

 //MARK: Properties
//use the same data as last time, one mile splits
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 practical 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)
}

We’ll find that the paceSeconds method is very useful as a class method. Change its declaration to

class func paceSeconds(pace:Int) -> String{

We now have several errors in our InterfaceController code, since we now need to use our model properly. Start by adding a runData property to InterfaceController:

var runData = RunData()

In the refreshTable method, change

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

to

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

and change

let paceString = "Pace:" + paceSeconds(data[index])

to

let paceString = "Pace" + RunData.paceSeconds(runData.data[index])

Our code now has a model separate from the controller. Let’s add one more method to figure average pace at a given split: Add this to the RunData class:

//find the total time run

//find the average pace by the mean of pace times
        //find the total time run
    func totalTimeforSplit(split:Int) -> Int {
        var total = 0
        for var index = 0; index >= split; index++ {
            total += data[index]
        }
        return total
    }
    //find the average pace by the mean of pace times
    func avgPace(split:Int) -> Int{
        let average = totalTimeforSplit(split) / (split + 1)
        return average
    }

For simplicity’s sake we’ll keep everything an integer in this example. For this example, the pace at my split was the pace I traveled for the mile I just ran. Totaling the splits up to a current split will give me the time running. If I divide that by the number of splits I ran, I get an average pace for the entire run so far.

Selecting Rows in a Table

Selecting row in a table is easy. You override the method table(table: didSelectRowAtIndex rowIndex:) method.

override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
    }

We’ll take our average pace and elapsed time and display them on a separate page, which will display when we select a row in the table. Add this code to the selection method:

//table selection method
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        //build a context for the data 
        var avgPace = RunData.paceSeconds(runData.avgPace(rowIndex))
        let context: AnyObject = avgPace as AnyObject
        presentControllerWithName("Info", context: context) //present the viewcontroller
    }

Using the value of rowIndex, We made a string for our context. I first get the value I need from the appropriate function, then convert it to a string with paceSeconds. I assign the string to the context as an AnyObject. Finally I present the view controller with the name Info with the context.

Of course, we haven’t made the interface yet. Go to the storyboard and drag out an interface. Drag two labels on top of the interface. On the upper label change the color to yellow or green, and a title of Average Pace. Make both labels have a width Relative to Container. Make the white one align center, with a title of 00:00:00. Click the view controller icon and set the title and identifier to Info. When done, you should have a view controller that looks like this one.

2015-08-12_09-28-09

Add a new file by pressing Command-N. Make a Cocoa Touch Class that subclasses WKInterfaceController called InfoInterfaceController. Be sure it is saved in the extension group by clicking the drop down menu on the save menu.

When the view controller appears, replace the class code with this:

class InfoInterfaceController: WKInterfaceController {

    @IBOutlet weak var paceLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        let pace = context as! String
        paceLabel.setText(pace)
    }
}

When the view awakes, we take the context, convert it back to a string and then place it in the label. Go back to the storyboard. Click the view controller icon on the info controller we just made. In the identity inspector make the controller InfoInterfaceController. Open the assistant editor, and drag from the circle in the view controller code to the white label until it highlights. Release the mouse button.

The simulator often has some hiccups, which stops your code from running. To prevent those hiccups, drag another interface on the storyboard. From the view controller icon on this blank controller, control drag to our table controller. Select next page drag the main arrow from the table controller to the blank controller. Your storyboard should look like this:

2015-08-11_05-41-46

We now start on a blank interface and swipe left to get to the table. This prevents whatever bug causes the simulator to hang or lose communication. Build and run. You should load to a blank screen:

2015-08-11_05-45-35

swipe to the left to show the table view:

2015-08-11_05-45-54

Click on the split for 10 miles, and you get an average pace.

2015-08-11_05-46-08

Click Info to return to the table.

Adding a Context Menu

In the rest of the lesson, we’ll add a few new functions to control the table. This is a good place to use a context menu. On the storyboard, drag a menu from the object library to the table controller, and drop on top of the controller

2015-08-11_05-50-43

If the document outline is not open, open it by clicking the icon in the lower left corner of the storyboard. In the document outline select the menu. In the attribute inspector, change the items to 3.

2015-08-11_05-51-28

We’ll use built-in icons for this application. Click the top menu item in the document, In the attributes inspector, change the title to Add Row and change the image to Add.

2015-08-11_05-57-03

For the second menu item, change the title to Delete Row and the image to Trash. For the last item change the Title to Reset and the image to Repeat.

Open the assistant editor if not open. Set the editor to Automatic. From the document outline, control drag from each of the menu items to the InterfaceController code. Make three actions addRow, deleteRow, and resetRows. Close the assistant editor for now.

Build and run. Go to the table controller, and hold down on the controller. The menu should appear:

2015-08-11_06-09-03

Adding Reset

We will be adding and deleting data in this demo. It will be helpful to add a reset method. Go to the RunData class. Add a class method like this.

//return the original array of Data
    class func resetData() -> [Int]{
        return [654,862,860,802,774,716,892,775,748,886,835]
    }

Since this is the same data in the array as the initialization, you can cut and paste that if you wish.

In InterfaceController, go to the menu action for the reset menu item. Change the code there to this:

    @IBAction func resetRows() {
        runData.data = RunData.resetData()
        refreshTable()
        selectedRow = nil
    }

The code reloads the array into the data property. We then refresh the table.

Selecting the Row

We have an error on the last line of this method. As a property, add the following to the InterfaceController class:

var selectedRow:Int! = nil

We’ll need to keep track of the row we last selected. That will be the row we’ll use to add or delete rows. However we may have an unselected state. To keep track of this we use an optional value. If the property is nil, there is no selection. In resetRows we reset everything, so we lose our selection, and set selectedRow to nil
in our table:DidiSelectRowAtIndex method, add the following line as the first line of code in the method:

 selectedRow = rowIndex //for use with insert and delete

Adding An Alert

Whenever we select a row, we set our selectRow property.
since we can have a nil value, we need to handle trying to delete nothing. We’ll need an alert to tell the user this won’t work. Go to the storyboard. Add another interface. In the attributes inspector, make the identifier No Splits Alert and the title Back.

2015-08-11_06-59-51

Add one label. Set the width to Relative to Container and align it Centered. Add text to the label so the interface looks like this:

2015-08-11_07-04-50

I went lazy here and used the built-in back button to handle dismissing the alert. If you want you can make another WKInterfaceController class and add a button to dismiss the alert. We’ll come back to this shortly.

Adding Delete

To delete a row, we delete the element in the array, then refresh the table. However, we also have to check for a nil value and handle those. Add this code to the deleteRow action in the Interface controller

    @IBAction func deleteRow() {
        if var row:Int = selectedRow{
            runData.removeItemAtIndex(row)
            refreshTable()
            selectedRow = nil
        } else {
          presentControllerWithName("No Splits Alert", context: nil)
        }
    }

We use optional chaining to make row from selected row. If nil, we present the alert. Otherwise we run a method in the model to remove the item, refresh the table, and set selectedRow to nil. In RunData, we need to add that removeItemAtIndex method:

    func removeItemAtIndex(index:Int){
        data.removeAtIndex(index)
    }

Build and run. Go to the table, and then the menu. Hit Delete Row and we get our alert:

2015-08-12_09-45-26

Go back to the table and select the 9 mile. Go back to the table, and then delete from the menu. The 9 mile is still there but the pace changes to what the 10 mile pace was.

Since we figured distance by the element number, our numbers mess up in this example. This is to keep things as simple as possible. If you wanted you could make a more robust model that had both miles and pace to prevent this problem.

Adding the addRow Functions

For adding we’ll do one of two things: if we select a row, we add that row at that index location. If we don’t select a row, we’ll add at the end of the splits, making a new finish time. But before we do we need to get what the pace is. Add this code to the addRow Action:

    @IBAction func addRow() {
        let context = self
        presentControllerWithName("Add Row", context: context)
    }

The action method sends us to another WatchKit interface where we’ll input the new pace information. There are several ways of entering information, but one that is easiest to validate is another table that lists possible pace times. We’ll select a time and use a delegate to add the row to the splits table. That’s why we set context to self.

The Add Item Function in the Model

We are trying to keep to MVC and all direct manipulations of the array happen in the model class. We need to add items to the array, so our model will need an add function.  Add the following code to the RunData class:

func addItemAtIndex(index:Int,item:Int){
        data.insert(item, atIndex: index)
    }

 

Make a Calculating Table

To make the add  row view controller, Let’s start with the code then connect it to an interface. Make another WKInterfaceController by pressing Command-N  for the keyboard shortcut or File>New>File on the menu bar. Make a new Cocoa Touch Class named  AddRowInterfaceController Subclassing WKInterfaceController. Make sure to save the controller in the WatchKit extension group.

We’ll also need a row controller like we did last time. Press Command-N and make a Cocoa Touch class AddRowTableRowController subclassing NSObject. Again, make sure this code ends up in the WatchKit extension group.

Change all the code for the row controller to:

import WatchKit

class AddRowTableRowController: NSObject {
   
    @IBOutlet weak var paceLabel: WKInterfaceLabel!
}

As a reminder, default NSObject templates import UIKit and not WatchKit. Xcode will not recognize the WKInterfacLabel as a class in your outlet unless you change UIKit to WatchKit.

Edit the AddRowInterfaceController. Add these properties to the class

 @IBOutlet weak var table: WKInterfaceTable!
    let minValue = 600
    let maxValue = 900
    var midIndex = 0
    var count = 0    

For the data, we’ll compute values for this table instead of having an array make them for us.  The computation is a simple one using a few properties. We have a two constants we use to set the minimum and maximum pace time in seconds. One of our properties count gives us a count of elements in our table. We will count from 0 to count using a for loop, making a value  rowIndex. For each row in our table we will take the  rowIndex and add minValue, ending up with a value between our minValue and our maxValue.

We want to scroll easily to the proper row. Starting in the middle will help this. The variable midIndex  gives us the center of the table, where we’ll go once the table finishes construction.

Add the code to make the table

 func makeTable(){
    table.setNumberOfRows(count, withRowType: "row")
        for var rowIndex = 0; rowIndex > count; rowIndex++ {
            let row = table.rowControllerAtIndex(rowIndex) as! AddRowTableRowController
            let paceString = RunData.paceSeconds(rowIndex + minValue)
            row.paceLabel.setText(paceString)
        }
        table.scrollToRowAtIndex(midIndex)
    }

What we did is make a loop with count number of rows. We set the number of rows then start the loop. In each iteration, we get the row controller, and place the pace inside of row’s label. That pace is the two values plus the minimum value. One the loop finishes, we scroll to the middle of the loop as our starting point.

Initialize everything and make the table in the awakeWithContext:

 override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        count = maxValue - minValue
        midIndex = (count / 2)
        makeTable()
    }

Selecting a row and Setting the delegate

We’ll select a row and then have a delegate send back the selection to our main table. Add the selection code

    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        let seconds = rowIndex + minValue
        delegate.didSelectPace(seconds)
    }

This uses a delegate method delegate.didSelectPace(seconds) which we now have to set up. If you are not familiar with delegates you may want to review here and here. Start by declaring the protocol above the AddRowInterfaceController class:

protocol AddRowDelegate{
   func didSelectPace(pace:Int)
}

Add a property for the delegate:

var delegate:AddRowDelegate! = nil

Then initialize the delegate from the context in AwakeWithContext:

delegate = context as! AddRowDelegate

We are done with the delegate in this class. Adopt the delegate in our InterfaceController class:

class InterfaceController: WKInterfaceController,AddRowDelegate {

Implement the required class like this:

    //delegate
    func didSelectPace(pace: Int) {
        if var index:Int = selectedRow{
            runData.addItemAtIndex(index,item: pace)
        } else {
            runData.addItemAtEnd(pace)
        }
        dismissController()
    }

With our pace date, we’ll either insert the row at the selected row or at the end of the table if we do not have a row selected, then we’ll dismiss the AddRowInterfaceController, leaving our new table for our inspection.

Story Board

We got this all coded, and now we are ready to work on the storyboard. On the storyboard, drag out an interface. In the Identity inspector, set the class to AddTableInterfaceController.

Drag on top of the interface a table. In the document outline click the row. Change the Identifier  to row, and in the identity inspector change the class to AddtableRowController. Add a label to the table. Make the Width and Height of the label Relative to Container.

2015-08-13_06-03-55

Open the assistant editor set to automatic. Drag from the circle next to the table outlet in your code to the table in the document outline.

2015-08-12_06-40-39

Set the assistant editor to the row controller by selecting Manual and then walking through the menus to the AddRowTableRowController.

2015-08-12_08-47-46

Drag the circle next to the outlet to the label in the document outline.

2015-08-12_06-44-15

We’ve set up everything. Build and run. Our table looks like this:

2015-08-13_06-22-49

Go to the menu and select Add Row.

2015-08-11_06-09-03

Select the time 12:30

2015-08-13_06-33-33

You will have a mile 11 with the finish data of 13:55 and Finish time with 12:30.  The data appended to the table  int he case of no selected split.

2015-08-13_06-33-45

Select Mile 9, which has a 12:28 pace.

2015-08-13_06-35-00

 Then exit from the average pace view, and add a 12:00 pace, scrolling up to find the 12:00.

2015-08-13_06-36-01

Mile 9 is now 12:00 and mile 10 is 12:28. We inserted the new pace data into the selected slot.

2015-08-13_06-36-21

This is a bit of a contrived example. It does show how to set up selection, deletion and addition of elements to a table. In the conclusion to the table series we’ll add headers, footers and sub-headers  to tables. We’ll learn how to use WatchKit’s way of handling more than one row controller.

The Whole Code

InterfaceController.Swift

//
//  InterfaceController.swift
//  watchkitTableVer1 WatchKit Extension
//
//  Created by Steven Lipton on 8/2/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation



class InterfaceController: WKInterfaceController,AddRowDelegate {

    @IBOutlet weak var table: WKInterfaceTable!
    //data is the pace in seconds per mile, taken every one mile except the last data point.
    //ver 2 -- moved all data to the model RunData
    var runData = RunData()
    var selectedRow:Int! = nil
    // 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(runData.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:" + RunData.paceSeconds(runData.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)
    }
    
    //table selection method
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        selectedRow = rowIndex //for use with insert and delete
        //build a context for the data 
        var avgPace = RunData.paceSeconds(runData.avgPace(rowIndex))
        let context: AnyObject = avgPace as AnyObject
        presentControllerWithName("Info", context: context)
    }
    override func awakeWithContext(context: AnyObject?) {
    
        super.awakeWithContext(context)
        
        // Configure interface objects here.
    }
    //MARK: Menus
    
    @IBAction func addRow() {
        let context = self
        presentControllerWithName("Add Row", context: context)
    }
    
    @IBAction func deleteRow() {
        if var row:Int = selectedRow{
            runData.removeItemAtIndex(row)
            refreshTable()
            selectedRow = nil
        } else {
          presentControllerWithName("No Splits Alert", context: nil)
        }
    }
    
    @IBAction func resetRows() {
        runData.data = RunData.resetData()
        refreshTable()
        selectedRow = nil
    }
    //delegate
    func didSelectPace(pace: Int) {
        if var index:Int = selectedRow{
            runData.addItemAtIndex(index,item: pace)
        } else {
            runData.addItemAtEnd(pace)
        }
        dismissController()
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        refreshTable()
    }

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

}

RunData.Swift

//
//  RunData.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class RunData: NSObject {
    //MARK: Properties
    //use the same data as last time, one mile splits
    var data = [654,862,860,802,774,716,892,775,748,886,835]
    //MARK: - Class Methods
    //return the original array of Data
    class func resetData() -> [Int]{
        return [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
   
    class func paceSeconds(pace:Int) -> String{
        let minutes = pace / 60
        let seconds = pace % 60
        return String(format:"00:%02i:%02i", minutes,seconds)
    }
    //MARK: - Instance methods
    func removeItemAtIndex(index:Int){
        data.removeAtIndex(index)
    }
    func addItemAtIndex(index:Int,item:Int){
        data.insert(item, atIndex: index)
    }
    func addItemAtEnd(item:Int){
        data.append(item)
    }
    //find the total time run
    func totalTimeforSplit(split:Int) -> Int {
        var total = 0
        for var index = 0; index <= split; index++ {
            total += data[index]
        }
        return total
    }
    //find the average pace by the mean of pace times
    func avgPace(split:Int) -> Int{
        let average = totalTimeforSplit(split) / (split + 1)
        return average
    }
}

InfoInterfaceController.swift

//
//  InfoInterfaceController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InfoInterfaceController: WKInterfaceController {

    @IBOutlet weak var paceLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        let pace = context as! String
        paceLabel.setText(pace)
    }
}

AddRowInterfaceController.Swift

//
//  AddRowInterfaceController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol AddRowDelegate{
   func didSelectPace(pace:Int)
}

class AddRowInterfaceController: WKInterfaceController {

    @IBOutlet weak var table: WKInterfaceTable!
    let minValue = 600
    let maxValue = 900
    var midIndex = 0
    var count = 0
    var delegate:AddRowDelegate! = nil
    
    func makeTable(){
    table.setNumberOfRows(count, withRowType: "row")
        for var rowIndex = 0; rowIndex < count; rowIndex++ {
            let row = table.rowControllerAtIndex(rowIndex) as! AddRowTableRowController
            let paceString = RunData.paceSeconds(rowIndex + minValue)
            row.paceLabel.setText(paceString)
        }
        table.scrollToRowAtIndex(midIndex)
    }
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        let seconds = rowIndex + minValue
        delegate.didSelectPace(seconds)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        count = maxValue - minValue
        midIndex = (count / 2)
        makeTable()
        delegate = context as! AddRowDelegate
    }

}

AddRowTableRowController.swift

//
//  AddRowTableRowController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class AddRowTableRowController: NSObject {
   
    @IBOutlet weak var paceLabel: WKInterfaceLabel!
}

TableRowController.swift

//
//  TableRowController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/2/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

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

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.