Tag Archives: Watchkit

Tables and Scroll Views in WatchOS2

One of the most powerful and important controls on both wearable and mobile devices are table views. Table views come in two flavors: static and dynamic. Dynamic table views read data from a collection type and displays it. Static tables allow for a vertical scroll view with a set of controls. Static table views are very often used as settings pages in applications. Once again, WatchOS goes for the simple route that we don’t get in iOS. In iOS’s UIKit we also have the more free-form scroll view. In WatchOS’s WatchKit, scroll views and Static table views are the same thing. They are created on the storyboard, while dynamic table views have some code but simplify the process compared to UIKit. In this lesson we’ll make scrolling behavior in the Apple Watch with tables and scroll views.

Make a New Project

Make a new project using the WatchOS application template iOS App with WatchOS App Name the project SwiftWatchKitTable, 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 First Controls

In the WatchKit app folder, select the Interface.storyboard. From the object library, drag a button on the WatchKit scene. Change the button’s label to Button 1

2016-03-17_06-04-23

Drag another 2 buttons, so we run out of room on the scene. Label them Button 2, and Button 3

2016-03-17_06-09-10

Break the Barrier

We’ve run out of space to put controls. Put another button, labeled Button 4 under Button 3. The scene stretches to include the button

2016-03-17_06-11-04

Set your simulator for a 38mm watch app.

2016-03-17_06-14-21

Build and run. On a 38mm watch the Button 4 slips slightly out of view

2016-03-17_06-21-32

Stop the watch app. Change to a 42mm watch face in the simulator. On a 42mm watch, the interface fits

2016-03-17_06-25-29

Add three more buttons to the scene by copy and paste. Select Button 4, press Command-C to copy. Press Command-V three times to makes three more buttons. Label them accordingly. The Interface continues to grow.

2016-03-17_06-29-16

Build and run again with the 44mm simulator. We start with the same controls as before, though we can see the edge of another button.

2016-03-17_06-34-18

In the watch simulator, click and drag up on the black background to see the hidden items. On the watch, you can just move the digital crown or do a drag up gesture.

2016-03-17_06-36-19

Add Separators and Labels

Scroll views and static table views are the same thing. Unlike iOS, there is no horizontal scroll, only vertical. To make it look more like a table view, you can add a few decorations to the interface. Find the separator object in the object library.

2016-03-17_06-40-05

Drag separators above and below the Button 3 like this:

2016-03-17_06-40-56

Add a label below the separators and one at the very top. Change the labels to Part1, Part2, and Part3.

2016-03-17_06-44-32

Build and run. Scroll down a bit and you’ll see your divided sections.

2016-03-17_06-48-12

Adding Groups

If you need to set up true sections, you can add groups as well. Below Button 2 add a group. Make the background Medium Green(#008000)

2016-03-17_07-02-45

Change the layout from Horizontal to Vertical

2015-07-22_06-21-32

Add a label with the text My Group, a date control and a switch to the group.

2016-03-17_07-02-46

Build and Run. Scroll down to see the group.

2016-03-17_07-07-59

Dynamic Table Views

If you have controls that don’t change, using the storyboard is the best way to have a series of controls. In the vertical, you are not limited by size as the watch automatically scrolls to accommodate your controls.

If you need to show a list of data on your watch, you will use a dynaimic table view. Dynamic table views follow this pattern. Instead of a special controller like UITableViewController in iOS, it is a control you place on the interface. You may be delighted to know that WKInterfaceTable is a lot simpler to put together than a UITableViewController: there are no delegate or data sources involved. It’s a very different experience than the iOS equivalent. It’s shrinks to one method you need to code. Since it is a control, you can easily mix other controls on the same interface as the table.

Modify the Project

On the Interface.storyboard, delete everything but two of the buttons. Make the button backgrounds Medium Blue(#8080FF). Title the top button Bottom and the bottom button Top

2016-03-17_07-28-43

Add a Table object

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

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

2016-03-17_07-32-42

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 . In this lesson, we’ll use only one. In the next lesson on advanced 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:

2016-03-17_07-38-10

Make a Row Controller

Our next step in building a table is making a table row controller. Tap Command-N to make a new file. Create a new WacthOS 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 iOS app.

2016-03-17_07-46-18

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

You can add actions, but for our basic example we’ll stick to just outlets. We’ll discuss actions in row controllers in the next part of the series.

Connect a Row Controller

Now we connect our row controller to the table. Go 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.

2016-03-17_07-52-02

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

2016-03-17_07-55-11

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 a running pace measured in seconds/mile. 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]

Pace data as an Int 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 hours = pace / 3600
        let remainingSeconds = pace % 3600
        let minutes = remainingSeconds / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours,minutes,seconds)
    }

Since I’m using integers, dividing by 3600 seconds give me hours. Taking the mod of 3600 gives me the seconds less than one hour. 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 InterfaceController.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 index in 0 ..< table.numberOfRows {

}

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:

let 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 index in 0 ..< table.numberOfRows {
            let row = table.rowControllerAtIndex(index) as! TableRowController
            let 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 tableRefresh method when we start the watch app. Change willActivate() to

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

It’s 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.

2016-03-17_08-18-34

The Bottom button is at the top of the table. Scroll down to the end:

2016-03-17_08-18-51

A Few Changes

There’s some labeling changes we need to make. We should use mile instead of miles in the first table entry. We also have a problem with the last entry in the table. Races and runs rarely end exactly at a mile marker. Most races are uneven miles (3.1, 6.2, 13.1, 26,2). It is likely 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 for 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
}

Change rowString from let to var

var rowString = String(format: "Split:%02i miles", index + 1)

Build and run. At the top we have mile.

2016-03-18_05-54-27

At the bottom, we have Finish.

2016-03-18_05-54-36

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. We might have a lot of data and want to get to the top quickly. The method scrollToRowAtIndex does this. We’ll take the two buttons we added and make them table navigation.

Go to Interface.storyboard and open up the assistant editor, set to Automatic so you see the InterfaceController class in the assistant window. Control-drag from the Bottom button to the code. Make an action toBottomAction. Control-drag from the Top button to the code. Make an action toTopAction. Add this code to the actions:

@IBAction func toBottomAction() {
    table.scrollToRowAtIndex(table.numberOfRows - 1)
}
    
@IBAction func toTopAction() {
     table.scrollToRowAtIndex(0)
}

The code table.scrollToRowAtIndex(0) scrolls us to the top of the table, which is index 0. Since rows begin with 0, we subtract 1 from the numberOfRows property of the table.
Build and Run.Tap the bottom button. We go to the last entry.

2016-03-18_06-22-41

Scroll up a bit to get the top button. Tap the button. We go to the first entry.

2016-03-18_06-22-55

You have a working table in WatchKit. It’s not very interactive or advanced. We have yet to add navigation or actions to table rows. While we can easily add headers and footers to the table as controls before or after the table, we don’t have and subheadings or subtotals. We might want different rows for different data. We’d also like to add and delete rows. In the next tutorial we’ll cover advanced table controller topics in WatchOS.

The Whole Code

InterfaceController.swift

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

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {
    var data = [654,862,860,802,774,716,892,775,748,886,835]
    
    @IBOutlet var table: WKInterfaceTable!
    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)
    }
    
    @IBAction func toBottomAction() {
        table.scrollToRowAtIndex(table.numberOfRows - 1)
    }
    
    @IBAction func toTopAction() {
        table.scrollToRowAtIndex(0)
    }
    
    func tableRefresh(){
        table.setNumberOfRows(data.count, withRowType: "row")
        for index in 0 ..< table.numberOfRows {
            let row = table.rowControllerAtIndex(index) as! TableRowController
            var rowString = String(format: "Split:%02i miles", index + 1)
            let paceString = "Pace:" + paceSeconds(data[index])
            if index == (table.numberOfRows - 1){ //table end
                rowString = "Finish"
            }
            if index == 0 {
                rowString = "Split:01 mile" //Table beginning
            }
            row.splits.setText(rowString)
            row.time.setText(paceString)
        }
    }
    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()
    }

}

TableRowController.swift

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

import WatchKit

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

How To Make Your First WatchOS2 App

It is a myth that using apps on an Apple watch requires a phone. This was true of Watch OS1, but not true of its successor OS. Granted, you still need a phone to load apps, but standalone apps are possible. With the introduction of WatchOS2, this becomes easier as the watch is less dependent on the phone than it was during the Apple Watch’s introduction. Sadly many developers have not realized this yet. You might still need your phone to load an app but independent apps of the phone are easier and more robust than ever under WatchOS2. The Apple Watch’s success is still being debated, but it relies greatly on the apps developed for it. In this new Series of tutorials we’ll discuss writing native apps for the Apple watch.

Introducing WatchOS1 and WatchOS2 through MVC

Understanding how to program for the Apple Watch is best described by a variation of the Model-View-Controller(MVC) pattern. It’s a programming pattern to keep one organized. There is a view which interacts with the user and a model that holds the data. We can diagram it like this:

MVC normal app

The controller in the middle keeps the model and view from talking to each other. They have to go through the controller. If any change happens to the view, the controller responds by changing the view or the model. In the same way if there is a change in the data, the controller will change what is necessary in the view. This keeps things modular and allows for the use of different views without a lot of extra coding or modification. If you need more of a detailed explanation of MVC you can find it here.

In WatchOS1 the Apple Watch is nothing more than a second view. It is not a computing device at all. It has its own mini-controller, but even that is on the phone in an extension, which is why in Watch OS1 you had to carry your phone for an app to work. We can diagram it like this:

MVC watchkit

The view controller of your app may talk to one of three types of view controllers (apps, glances and notifications) in extensions. The extension transmits just enough from your phone to the watch to make the view. The extension controller can respond to events from the watch or display things. Compared to a scene on a storyboard, it is a limited number of things we can display or sense too.

This changes in WatchOS2. The extension moves on board the watch.

MVC WatchOS2 Basic

The watch app thus runs on the watch not the phone. With a few important exceptions, you don’t need the phone to run a watch app.

What didn’t change is the WatchKit library. Watchkit is the equivalent of UIKit in iOS, giving us our view objects like buttons and labels. WatchKit has seemingly many familiar objects from UIKit to place on the display — until you start to work with them. In many ways, WatchKit is UIKit super-ultra-mega-lite. Most of what you know from iOS has been paired down to a bare minimum. Many attributes can only be set on the storyboard, and not programmatically. There are many write-only objects, such a labels. You can not get a label’s text, only set it.

In this series of articles on WatchOS, I’m going to go show you how to use Xcode and the Watch API’s. We’ll start with a few demonstration apps, then begin to build a full app for the fitness market in later tutorials.

Before You Begin: Setting up XCode and the Simulators

For this tutorial I’m using Xcode 7.3 beta, with WatchOS2.2. The watch simulators in Xcode can be kind of cranky and slow, so its a good idea to pre-load your simulators before opening Xcode. I’ll assume you’ve loaded a copy of Xcode 7.2 or 7.3beta for this tutorial. I make this easier by keeping them in the dock once I start them up, but this first time is a little more difficult, since I start them manually. Navigate to the applications folder by clicking Applications in the dock, and click the open in finder icon. In Finder, find the Xcode or Xcode-beta application, depending which one you plan to use. Right-click on the file and select Show package Contents

Finder will show you a directory Contents. Navigate to ./contents developer/applications.

2016-02-03_08-11-13

Run both the Simulator and the Simulator(Watch). Once you started both of them, Right-click on their icons in the dock, and select Options>Keep in Dock. Every time you start Xocde, Start them before you start Xcode to give them time to boot up.

A Sample Project:Hello Pizza Run

Like many first apps in a new language, we’ll do a variation on the classic “hello world” program. I like to eat pizza or run when I’m not behind the keyboard. For the first app in our series, we’ll go through setup of a WatchKit app, and switch between these two activities.

Open Xcode if not already open. Create a new project and pick the iOS app with Watchkit App Watch project template.

2016-02-03_05-34-40

You’ll get a configuration window like this:

2016-02-03_05-35-57

Name the project HelloRunPizzaDemo using Swift as the language. Use a Universal device and have Include Notification Scene checked as shown above. Look at the Navigator and you will see some unfamiliar folder. Open them up and you’ll find all these files:

2016-02-03_05-37-22

We have one folder for the WatchKit extension, and one for the WatchKit app. The controller is in the extension, and the storyboard with the views is in the WatchKit App. You only have a view in a watch app. Click on the Interface.storyboard in the HelloRunPizzaDemo WatchKit App group. You will get a blank watch scene with a watch controller and a notifications controller.

2016-02-03_05-37-58

We’ll ignore the notification controller for now. You will find in the object library some old friends from the iPhone and iPad storyboard. Find the label and drag out two labels. Then find the button and drag out a button. You’ll immediately notice a difference in the storyboard. The labels stack.

2016-02-03_05-39-04

WatchKit uses a different layout system than iOS storyboards. This is the early, simpler version of what would become stack views in iOS. It will stack items below each other unless you give them specific alignment instructions in the attributes inspector. If two labels are in the same position, they stack. Select the top label. In the attributes inspector find Alignment

2016-02-03_05-41-04

Currently it is the left top position. Click the Horizontal drop down and you will see it has choices for Left, Center and Right. Select Center. Click Vertical and you will see Top, Center and Bottom. Leave it Top. Now select the other label, and Center both horizontally and vertically. Click the button and align it Center horizontally and Bottom vertically. Double click the top label and change the label to read Hello Pizza!!. Double click the button and change its title to Change. In the button, change the text color to Dark Text Color. Change the background for the button to #AACC00. If you are not familiar how to do this click on the Default part of the drop down for the background color in the attributes inspector. Select Other Color…. You will get a color palette window. Select the sliders and Select RGB Sliders.

2016-02-03_05-46-11

Change the Hex Color to AACC00. Close the palette You should now have this:

2016-02-03_09-00-15

Using Emoji in an App

A simple way to add graphics to the watch is to use Emoji. Double click in the second label. Press Control-Command-Spacebar together. You will get the special character window:

Screenshot 2015-04-12 13.41.12

On the right of the window select Emoji and then find Food and Drink.

Screenshot 2015-04-12 13.41.19

Select the Slice of Pizza and then double-click the selection. The pizza appears in the label. Close the special character window for now. Select the pizza emoji on the storyboard. In the attribute inspector change the Font to System and make the Size 60 points to make the pizza bigger. You’ll end up with this:

2016-02-03_05-45-42

First Run

Before we test run our app, we need to change the run scheme. The simulator is set to run the iOS app, not the watch app. We need to tell Xcode to run this on the watch simulator. At the upper left of the Xcode window you will find this:

2016-02-03_09-28-22

Change the scheme to Watchkit app > Apple watch 42mm.

2016-02-03_05-49-38

Watch apps in the simulator can get cranky. It’s a good idea to clean the app before you build and run each time. Press Command-Shift-K or in the menu Product>Clean.

2016-02-03_05-46-34

Build and run. Once the app compiles and begins to run you will see the watch simulator as a watch

2016-02-03_05-50-43

It will eventually start the app with a loading screen

2016-02-03_05-51-18

Finally, it will show our app

2016-02-03_05-51-40

Connect Actions and Outlets

Let’s connect some outlets and actions to the app. Stop the simulator. Go back to XCode. With the storyboard showing, open the Assistant editor. It will show a file InterfaceController.swift which is in the extension group. Control-drag the Hello Pizza!! label from the storyboard to the code. We get an outlet dialog box.

2016-02-03_05-57-13

Name the outlet pizzaRunLabel. Make another outlet for the pizza emoji, naming the outlet pizzaRunEmojiControl drag from the button. Change the dialog box to an action.Make an action named changeButton.

2016-02-03_05-56-21

Add Constants and a Variable

We’ll add two constants and a Bool variable for this code. Above the outlets add this code:

let pizzaEmoji = "" //pizza emoji character goes here
let runEmoji = "" //runner emoji character goes here
var running = false

Click between the quotes in the string for the Pizza emoji. As we did before with the label, put a Pizza Emoji as the string. For our second emoji, add a runner. In the current versions of iOS and OSX, you can change the skin color on people emoji. With the characters window still open, select People and find the runner. Click and hold on the runner and you will get a pop up menu with different color runners.

2016-02-03_06-00-21

Click on a runner you like and it will change color. Double click the runner to get the runner character in the runEmoji string.

Code the Action

Add the following code to the action.

@IBAction func changeButton() {
    running = !running
        if running {
            pizzaRunLabel.setText("Hello Running")
            pizzaRunEmoji.setText(runEmoji)
        }else{
            pizzaRunLabel.setText("Hello Pizza")
            pizzaRunEmoji.setText(pizzaEmoji)
        }
}

We use a method setText to set the text of the label. One of the biggest difference between WatchOS and iOS is the lack of properties in WatchKit. Here instead of setting a property text, we use a method setText(string:).

Clean the code with Command-Shift-K and then build and run. When our apps finally runs, we get this the same as we did last time.

2016-02-03_05-51-40

But now we press the button, and it changes

2016-02-04_07-43-20.png

Notifications and the simulator

While we’ll cover how to use notifications later, sometimes the watch simulator gets really cranky for no reason. One way to get around this is to launch the app from the notification instead of the app. Stop the simulator. In the devices, change from a watch app to a notification, again using a 42mm watch.

Clean, build and run. About 30 seconds to a minute after the watch simulator starts you will get a notification screen.

2016-02-03_10-38-22

Tap the button, and the app launches.

2016-02-03_05-51-18 2016-02-03_05-51-40

We’ll talk more about notifications in a later tutorial. In the next WatchOS2 tutorial, we’ll discuss timers.

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: Headers Footers and More — Multiple Row Types in Apple Watch Tables

2015-08-19_10-10-23

Some tables are boring. In our  multi-part look at the table view in Apple Watch, We’ve looked at tables with only one kind of row type. However, tables can be lot more than just one row. We might have a header row, or a footer row, we may have sub rows to do some grouping or subtotals. In this lesson we’ll learn how to add those rows into a table.

In the first lesson of the table series we introduced the setNumberowRows:withRowType method to make a table with a single row type. For example in the second lesson where we manipulated table data,  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:

2015-08-17_05-48-09

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

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

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

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

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 setup. 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. 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. In the rest of this lesson, we’ll explore ways to be flexible enough to handle anything thrown in our direction.

Set up the project

For this part of the lesson we’ll start from scratch. Open Xcode and press Command-Shift-N to make a new iOS Single View Project.  Name the Project WatchKitMultiRow with Swift as the language. Save the project where you want.

Once the project loads, go to Editor>Add Target… Add a WatchKit App Target. In the next screen, check off the notifications, and activate the target

Add the Model and Model Methods

Go to the extension group in the navigator, and select the InterfaceController.swift.  Add the following two lines just under the interface  controller’s class declaration:

var rowTypes = ["Header","Sub Header", "Row","Row","Sub Header", "Row","Row","Footer"]
var data = [740,745,750,740]

Row one is our row types array, and line two is our data. We’ll add a few methods to use in our headers and footers:

    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
        var minutes = (pace - (hours * 3600 )) / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours, minutes,seconds)
    }

The first method avgData finds the average of the range of data. The paceSeconds method formats the number of seconds we use as data into a string of the type HH:MM:SS.

Set Up the Storyboard

Go to the storyboard in the App Extension. Find the table controller. Drag the controller on the interface.

2015-08-20_05-38-52

If not open, open the document outline. You will see the table in the document outline. If not selected, select it.

2015-08-20_05-41-29

In the attributes inspector, change the number of rows from 1 to 4,

2015-08-17_07-02-21

This is badly labeled.  Rows is not the number of rows in your table. This is the number of row types you will use in the table.  In the storyboard you now have four groups on your table labeled Table Row:

2015-08-17_07-03-17

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

2015-08-17_07-16-28

Click on the top Table Row Controller in the document outline. In the attribute inspector, change the Identifier for the row controller to Header. Also deselect the Selectable check box.

2015-08-17_07-23-54

  It will change in the document outline to Header. Change the rest of the table row controllers to  Subhead, Row and Footer. For Subhead and Row, leave the Selectable Box checked. For Footer, uncheck it. Selectable lets the user select the row and then run code on it. We do not want that for our header and footer. Later on,  we will add  actions with the subhead and row.

Your document outline should look like this:

2015-08-17_07-24-51

 Drag into each of the four controllers on the storyboard a label. For each label, change the width to Relative to Container. 

2015-08-18_05-45-50

Change the text on the top label to Head,then the next three to SubHead, Row, and Foot. In the font attribute, change the font on the Head and Foot to Headline.

2015-08-18_05-47-42

Change the font on the SubHead to Subhead.  Change the colors of the group to red for the header, green for the subheader, and blue for the footer.

2015-08-18_05-54-27

For the Head, Subhead and Foot’s group change the Height to Size to Fit Content .

2015-08-20_05-13-49

When done, your table should look like this.

2015-08-20_05-24-21

Add TableRowControllers

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

In the code change import UIKit to import WatchKit. Repeat this three more times, making a SubHeadTableRowController, RowTableRowController,and FootTableRowController.

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

If not visible, open the outline for the Header 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 an outlet Named label.

2015-08-18_06-16-36

Do the same for the other three  row controllers, each time making a outlet named label.

Select the  interface controller for the table  in the document outline.  Make sure the identity inspector has InterfaceController for the class. If not, change it. Select the table and set the assistant editor to Automatic. Control drag the table to the class, making an outlet table.

Close up the assistant editor.

Iteration 1: Add the Table Creation Method

We are going to show two different methods for creating the table. The first assumes you set rowTable manually. Go to the InterfaceController class. Add this code to the class:

  func refreshTable(){
        var dataIndex = 0
        table.setRowTypes(rowTypes)
        for var rowIndex = 0; rowIndex < rowTypes.count; rowIndex++ {

        }
    }

Line 3 sets this as a multi-row controller table. As in earlier lessons, we’ll loop through an array and set each element of the array. Unlike previous lessons, we are not iterating through the data array but the rowtypes array. We’ll need some place marker for where we are in the data and thus we have a variable dataIndex to keep track.
Each row type has its own way to present data on its own controller. Using rowTypes[rowIndex], we’ll make a row with the right controller then present the data. This is a good place to use a switch statement. Inside the for loop add this code:

switch rowTypes[rowIndex]{
    case "Header":
        let row = table.rowControllerAtIndex(rowIndex) as! HeadTableRowController
        row.label.setText(String(format:"Count: %i",data.count))
    case "Sub Header":
        let row = table.rowControllerAtIndex(rowIndex) as!  SubHeadTableRowController
        let avg = avgData(data, start: 0, end: dataIndex)
        row.label.setText(String(format: "Avg Pace: %i", avg))
    case "Row":
        let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
        row.label.setText(String(format: "Pace %i Sec", data[dataIndex++]))
    case "Footer":
        let row = table.rowControllerAtIndex(rowIndex) as! FootTableRowController
        let avg = avgData(data, start: 0, end: data.count - 1)
        row.label.setText(String(format: "Avg Pace: %i", avg))
    default:
         print("Not a valid row type: " + rowTypes[rowIndex]   )
}

Note the common code here, for the proper row type we get a row in that row table controller class. Then we use the methods there to populate the row.  The header will have a count of our data, the subhead the current average  pace of the run, the row the current pace data and the footer the overall average. Call the method in the willActivate method:

   override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        refreshTable()
    }
  

Build and run.

2015-08-19_10-15-15

scroll down to see the footer.

2015-08-20_06-27-52

Iteration 2: Add a Flexible 2-pass Method

The code above is great if you know what your row types  are when coding. In our code so far we know we have four data points and two subheadings. We rarely know that. We’ll need a more flexible solution to the problem than the code above. We have to create that rowTypes array before we make the table.

The New Model

We have two arrays at the moment: rowtypes and data. As discussed before, these do not have a 1:1 index relationship. The rowindex[2] is where data[0] is. Our table would be a lot easier to handle if rowIndex has the same index as our data. We do that by making another array. Let’s call it rowData. This array rowData will store whatever data we need for that row.
Add this to our code property declarations

var rowData:[Int] = []

We’ll make this a integer array for now. we’ll store what we are displaying in the labels for the row in it. In our next lesson we’ll see why we may want to make this something more.

The build method

Since we have rowData, we build our table logically in rowType and rowData. We have to do the following:

  1. Make a header row
  2. Loop through the following
    1. At some condition, make a sub header
    2. Make a Row from data
  3. Make a Footer

At each of these steps, we’ll append a row type to the rowtypes array and row data to the rowData array. Add this method:

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

We use this in the buld methd. Add the following method for our build of the table:

 func buildTable(){
        //clear the arrays
        //make a header
        //loop through the data
        for var index = 0; index < data.count; index++ {
        //if we are on an even row except 0, add a subhead/foot
        // add the row data
        }
        //add the footer
    }

This is the outline we made above. What we will now do is flesh out this structure. Add to this method to clear the arrays:

//clear the arrays
        rowTypes = []
        rowData = []

Add the header, with the number of data items as our data

        //make a header
        addRowWithType("Header", withData: data.count)

Inside the loop we have the header and subhead. In this example, the header is  a summary row between every two rows. Change the loop to this:

//loop through the data
let subHeadInterval = 2
for var index = 0; index < data.count; index++ {
    //if we are on an interval row except 0, add a subhead/foot
    if index % subHeadInterval == 0 && index != 0{
         addRowWithType("Subhead", withData: avgData(data, start: index - subHeadInterval, end: index - 1))
     }
     // add the row data
     addRowWithType("Row", withData: data[index])
}

To make this more flexible, we’ll use a constant subHeadInterval to tell us how many rows to display before displaying a subhead. We use the modulus operator % to tell us when we get to that row. In that subhead we figure the average pace for the last split. For every time through the loop, we add a row.

Last we have the footer.  Add this for the footer in our method:

//add the footer
        addRowWithType("Footer", withData: avgData(data, start: 0, end: data.count - 1))

The Refresh Table Method

We have two arrays with a common index.  Use those arrays to make a table. Much of this is the same as before. Add another method to our code:

    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]))
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                row.label.setText("Avg Pace: " + paceSeconds(rowData[rowIndex]))
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText(paceSeconds(rowData[rowIndex]))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                row.label.setText("Pace: " + paceSeconds(rowData[rowIndex]))
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }

This is very similar to the refreshTable() we already wrote. The difference is the lack of calculations — that’s all done in buildTable. Here we only format and display our results.
change the willActivate() to this

//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()
    }

Build and run. We get similar results to our first attempt.

2015-08-20_06-39-51

then change the data

//var data = [740,745,750,740]
var data = [740,745,750,740,760,765,770,755]

This would have messed up refreshTable. but refreshBuildTable works fine. Build and run. You have a longer table.

2015-08-19_10-10-23

There’s a few things about this table we still need to do. I’d like to be able to know what mile the splits are for in my sub head. I’d also like to be able to display a lot more data about my run at every row. We’ll do that in our next lesson, where we’ll learn how to select in a multi-row table, and use a custom data type that give us a lot more flexibility in our information.

The Whole Code

InterfaceController.swift

//
//  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 InterfaceController: WKInterfaceController {
    
    @IBOutlet weak var table: WKInterfaceTable!
    var rowData:[Int] = []
    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){
        rowTypes = rowTypes + [type] //append the rowtype array
        rowData = rowData + [data] //append the rowData array
    }
    
    func buildTable(){
        //clear the arrays
        rowTypes = []
        rowData = []
        //make a header
        addRowWithType("Header", withData: data.count)
        //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))
            }
            // add the row data
            addRowWithType("Row", withData: data[index])
        }
        //add the footer
        addRowWithType("Footer", withData: avgData(data, start: 0, end: data.count - 1))
    }
    
    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]))
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                row.label.setText("Avg Pace: " + paceSeconds(rowData[rowIndex]))
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText(paceSeconds(rowData[rowIndex]))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                row.label.setText("Pace: " + paceSeconds(rowData[rowIndex]))
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }
    
    //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()
    }
}

HeaderTableRowController.Swift

import WatchKit

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

}

SubheadTableRowController.Swift

import WatchKit
class SubheadTableRowController: NSObject {
    
    @IBOutlet weak var label:WKInterfaceLabel!

}

RowTableRowController.Swift

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

}

FooterTableRowController.Swift

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.

 

Swift Watchkit: Making ScrollViews and Static TableViews.

To state the obvious, The Apple Watch has very small  screen real estate. There are times we need more screen space than is available. In iOS, there are scroll views. One subclass of scroll views are the table views. Table views come in two flavors: static and dynamic. Dynamic reads data from a collection type and displays it accordingly. Static tables allow for a vertical scroll view with a set of controls. Static table views are very often used as settings pages in applications.   Once again, WatchKit goes for the simple route that we don’t get in iOS.  Scroll views and Static table views are the same thing. What’s more you do everything in the storyboard — no coding necessary.

Make a New Project

Make new project called SwiftWatchKitScroll, 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.

Add Your  First Controls

In the WatchKit app folder, select the storyboard. From the object library, drag a switch on the WatchKit scene. Change the switch’s label to Option 1

2015-07-21_07-24-58

To speed things up I’m keeping the defaults for position and size for my controls. Drag another  switch and then a button, so we run out of room on the scene:

2015-07-21_07-24-12

Label the switch Option 2 and the button Button1.

Break the Barrier

We’ve run out of space to put controls.  Put another Switch  under the button. Label it Option 3. The scene stretches to include the button

2015-07-21_07-23-31

Build and run. On a 38mm watch the Option 3 label slips slightly out of view, on a 42mm watch, the interface fits

2015-07-22_05-50-49 2015-07-22_05-52-11

Add more controls to the scene.  I added another switch button, a slider and another button

2015-07-21_07-22-40

Build and run again. we start with the same controls.

2015-07-22_05-59-54 2015-07-22_06-01-01

On both the 38mm and 42mm watch simulator, you will be able to drag up  by clicking and dragging  on the black background to see the hidden items. On the watch, you can just move the digital crown or do a drag up gesture.

2015-07-22_06-03-54 2015-07-22_06-00-24

Add Separators and Labels

This is the basics for any scroll view and static table view. They are really the same thing. To make it look more like a table view, you can add a few decorations to the interface.  Drag separators above and below the Option 3 switch like this:

2015-07-21_07-21-53

Add a label below the separators and one at the very top. Change the labels to Part1, Part2, and Part3.

2015-07-21_07-21-22

Build and run. Scroll down a bit and you’ll see your divided sections.

2015-07-22_06-33-30

Adding Groups

If you need to set up true sections, You can add groups as well. Below Button2 add a group.

2015-07-22_06-21-05

Change the layout from Horizontal to Vertical

2015-07-22_06-21-32

Change the background color of the group. I made mine 50% gray (#808080)

2015-07-22_06-23-07

Add some controls, a separator and label to the group.

2015-07-22_06-30-31

Build and Run. Scroll down to see the group at the end of the scroll.

2015-07-22_06-34-00

This was short and rather simple lesson. To get scrolling behavior, all you need to do is add more controls, and set outlets for each of them. One last point: a watch app interaction lasts only a few seconds. Put your most important information at the top of a scroll so users can look and change it quickly. Put the least important at the bottom.

In our next lesson, we’ll look at the dynamic table in WatchKit.