Tag Archives: select rows

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!
}