Tag Archives: Programming Apple watch

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

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.

Swift Watchkit: Working With Modal Views Part 3: Modal Page Views

Photo Jun 11, 6 33 39 AM

Apple’s  documentation for WatchKit  is quite clear, even when it is lying. The documentation states you can have  hierarchical navigation or page navigation but not both. Here’s is where it lies:  you can have a page-based navigation as part of a hierarchical navigation scheme. There is a special modal view version of page views, which you can use in a hierarchical scheme.  In the conclusion of this lesson on modal views, we’ll look at  using the modal  page view.

In the last part, we learned we can call a modal view programmatically using the method presentControllerWithName(_name:, context:). For example, we had this code for presenting the number controller modally:

@IBAction func numberButtonPressed() {
    //myModel.myNumber = 3.14
    myModel.delegate = self
    //prepare the context
    let context: AnyObject? = myModel
    //present the controller
    presentControllerWithName("Number", context: context)
}

For a pages control, we just make everything in the modal control plural. The method is presentControllerWithNames(_names:, contexts:). For a modal page controller, use arrays instead of a single values for the parameters. In the code from the previous lessons add this under the numberButtonPressed action.

@IBAction func pagesButtonPressed() {
    let pageNames = ["RunPage","WalkPage","PizzaPage"]
    let pageContexts:[AnyObject]? = [myModel.myNumber,myModel.myColorString,myModel]
    presentControllerWithNames(pageNames, contexts: pageContexts)
 }

Line 1 has an array of interface identifiers. The system will present the pages in this order, starting with the Run page, going to the Walk page and finishing with the Pizza Page. Line 3 gives different contexts for the corresponding page. Our Run page for example will take the Float value myNumber from the model. Similarly, the Walk page will take a String. Let’s design a storyboard then write some code to see how this would work.

Adding the Pages Button

Go to the watch app storyboard. On the I am Root interface, add another button, which will snap itself to the bottom. Label it Pages.

2015-06-11_07-32-21

Open the assistant editor. Next to the action pageButtonPressed is the open circle. Drag from the circle to the Pages button to connect the button.

Adding the Three Page Controllers.

Press Command-N and make a new controller subclassing WKInterfaceController named RunPageInterfaceController. Be sure to group it in the WatchKit extension. Do the same to make two more interface controllers named  WalkPageInterfaceController and PizzaPageInterfaceController.

Go back to the storyboard.  Add a new interface to the storyboard. Add a group on the interface.

2015-06-11_06-38-13

Select the group. In the attribute inspector, set the Height  to Relative to Container.  Change the color of the group to Light Gray.  Change the Layout to Vertical.

2015-06-11_06-38-58

Add two labels to the group

2015-06-11_06-40-54

Select the bottom label. Set the Position to Horizontal: Center, Vertical:Center. Change the font to 50 point System Centered.

2015-06-11_06-40-17

Select the controller icon on this interface. Press Command-C to copy the interface.  Click on the white background of the storyboard to deselect everything. Press Command-V to paste a copy of the interface.  The pasted interface is on top of the current interface. Drag the interface and you will now have two.

2015-06-11_06-47-12

Deselect everything again and press Commmand-V. Drag again and you will have three controllers

2015-06-11_06-47-55

In the identity inspector, Make the ID of one Controller RunPage and its name Run. Make the ID of the second controller WalkPage, and the name Walk. Make the ID of the third PizzaPage and the name Pizza.  Using emoji you can find at Control-Command-Space, set up the three interfaces to look like this:

2015-06-11_06-43-54

Go to the identity inspector. Give RunPage a class of RunPageInterfaceController, WalkPage a class of WalkPageInterfaceController, and PizzaPage a class of PizzaPageInterfaceController.

Build and run.  Press the Pages button  and the Run interface shows. Swipe to the left and you get the walk page, another swipe give you the Pizza Page. Tap on the word Pizza on the dark background. The modal dismisses.

Adding Contexts

While you can add delegates to each page. You don’t have to. Modal pages very likely won’t interact, but just show information, like glances. This time, we’ll transmit information through the context and use it differently in each page.

Wire up the  Page controllers.  With the assistant editor open, select the RunPage interface. Control-Drag the smaller label to the  code, just under the class declaration.  Add an outlet statusLabel .  After the outlet, add a property:

var number:Float = 0.0

In the code, add the following to awakeWithContext:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    //Update the status label with the context.
    number = context as! Float
}

The RunPage is the first element of the context  array. We set the context there to the Float value of  myModel.myNumber. We only have to unwrap the Float value to use it. We’ll display our result in willActivate. Change that method to this:

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    let displayString = String(format:"Run %0.2f minutes",number)
    statusLabel.setText(displayString)
}

The code changes the number to a String, then displays the string on the label.

  We’ve finished our first controller.  With the assistant editor open, click on the WalkPage in the storyboard. The WalkPageController should be in the assistant editor. In this controller, we take a String with a color for our context. We’ll instantiate a new instance of the model, then add that string to the model to compute a UIColor for the string. We’ll use that model to change the color of the group’s background and to tell us what color we have.

We need two outlets for this.  Control-drag from the group to the code in the assistant editor. Add an outlet named backgroundGroup. Control-drag from the label to the code. Add an outlet named statusLabel. Finally add an instance of  DemoModel named myModel. You should have just added this code:

@IBOutlet weak var backgroundGroup: WKInterfaceGroup!
@IBOutlet weak var statusLabel: WKInterfaceLabel!
let myModel = DemoModel()

We’ll cast the context’s AnyObject? to type String, then add to the model with the colorFromString method on the model we used in the first lesson on modals. Change awakeWithContext  to the following code:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
   // Configure interface objects here.
   //Context is a String, but we need a string and color
    //so we use a new model.
    let colorString = context as! String
    myModel.colorFromString(colorString)
}

Once again, set up the display in the willActivate code:

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    let displayString = myModel.myColorString + " Walking"
    statusLabel.setText(displayString)
    backgroundGroup.setBackgroundColor(myModel.myColor)
}

Click on the pizza interface to select it and bring up the PizzaPageInterfaceController code in the assistant editor. This time we pass DemoModel as the context. Wire up the controller by first adding the following outlets and actions to the code:

var myModel = DemoModel()
@IBOutlet weak var backgroundGroup: WKInterfaceGroup!
@IBOutlet weak var statusLabel: WKInterfaceLabel!

Drag from the empty circle to the left of the backgroundGroup to a spot on the Pizza interface. Release the button. Do the same between statuslabel and the label.

We have only one line to add to awakeToContext. We’ll need to cast the context to a DemoModel object.

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    myModel = context as! DemoModel
 }

Since we are pizza themed in this page, let’s have a different pizza based on color. Change willActivate to the following:

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        var pizzaType = "Cheese"
        switch myModel.myColorString{
            case "Red": pizzaType = "Pepperoni"
            case "Green": pizzaType = "Veggie"
            case "Blue": pizzaType = "Gorgonzola"
            default: pizzaType = "Cheese"
        }
        let displayString = pizzaType + " Pizza!!"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

We created a variable pizzaType.  Using a switch statement, we mapped the colors to appropriate types of pizzas (Red meat, Green vegetables and Blue cheese), assign the pizza type in pizzaType.  Instead of displaying the color, we display  the pizza.

Build and run. Assuming the simulator behaves itself. you should have your three buttons.

2015-06-11_06-57-58

Set the number  to 3.5 by pressing the number button and using the slider, then pressing Done.

2015-06-11_06-58-34

Set the color to Blue in the Color modal.

2015-06-03_06-13-02

When done your watch looks like this:

2015-06-11_07-01-41

Tap Pages. The Run Page appears:

2015-06-11_06-58-57

You’ll notice our number is in the label. Swipe left to get the walk page and our selected color: .

2015-06-11_07-02-38

Swipe left again to get a pizza based on the blue color:

2015-06-11_07-02-02

Our modal controllers work perfectly. Modals are meant for interrupted use of a more active view. Page modals are best used for viewing data in a glance fashion. Actually glances are a type of modal controller.  Modals are the last of the basic types of controllers in Watchkit. In future lessons we’ll use these with a few very handy complex controllers. In the next lesson, we’ll look at  one of these: Text input when you don’t have room for a keyboard.

The Whole Code

Storyboard

2015-06-15_07-37-17

InterfaceController.swift

//
//  InterfaceController.swift
//  SwiftWatchModalDemo WatchKit Extension
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController,ColorModalDelegate, NumberInterfaceDelegate {
    
    //MARK: Outlets and Properties
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    @IBOutlet weak var colorButton: WKInterfaceButton!
    var myModel = DemoModel()
    
    //MARK: - Actions
    
    @IBAction func numberButtonPressed() {
        //myModel.myNumber = 3.14
        myModel.delegate = self
        //prepare the context
        let context: AnyObject? = myModel
        //present the controller
        presentControllerWithName("Number", context: context)
    }
    
    @IBAction func pagesButtonPressed() {
        let pageNames = ["RunPage","WalkPage","PizzaPage"]
        let pageContexts:[AnyObject]? = [myModel.myNumber,myModel.myColorString,myModel]
        presentControllerWithNames(pageNames, contexts: pageContexts)
    }
  
    //MARK: - Delegates
    func colorDidFinish(context: AnyObject?) {
        myModel = context as! DemoModel
        dismissController()
        updateDisplay()
    }
    
    func numberDidFinish(context:Float){
        myModel.myNumber = context
        updateDisplay()
        dismissController()
    }
    
    //MARK: - Instance Methods
    func updateDisplay(){
        let statusText = String(format: "%1.2f", myModel.myNumber)
        statusLabel.setText(statusText)
        colorButton.setBackgroundColor(myModel.myColor)
    }
    
    //MARK: - Life Cycle
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        if segueIdentifier == "ColorsModal" {
            myModel.delegate = self
            return myModel
        }
        return nil
    }
    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()
        updateDisplay()
    }

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

}

DemoModel.swift

//
//  DemoModel.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class DemoModel:NSObject{
    var myNumber:Float = 0.0
    var myColorString = "Black"
    var myColor = UIColor(white: 0.2, alpha: 0.73)
    var delegate:AnyObject? = nil
    
    func colorFromString(colorString:String){
        myColorString = colorString
        switch colorString {
        case "Red": myColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.73)
        case "Green":myColor = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.73)
        case "Blue":myColor = UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.73)
        default: myColor = UIColor(white: 0.2, alpha: 0.73)
        myColorString = "Black"
        }
    }
}

WalkPageInterfaceController.swift

//
//  WalkPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class WalkPageInterfaceController: WKInterfaceController {

    @IBOutlet weak var backgroundGroup: WKInterfaceGroup!
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    let myModel = DemoModel()
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    
        // Configure interface objects here.
        //Context is a String, but we need a string and color
        //so we use a new model.
        let colorString = context as! String
        myModel.colorFromString(colorString)
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        let displayString = myModel.myColorString + " Walking"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

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

}

RunPageInterfaceController.swift

//
//  RunPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class RunPageInterfaceController: WKInterfaceController {

    var number:Float = 0.0
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
        //Update the status Label with the context. 
        number = context as! Float
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        let displayString = String(format:"Run %0.2f minutes",number)
        statusLabel.setText(displayString)
    }

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

}

PizzaPageInterfaceController.swift

//
//  PizzaPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class PizzaPageInterfaceController: WKInterfaceController {
    var myModel = DemoModel()
    @IBOutlet weak var backgroundGroup: WKInterfaceGroup!
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        var pizzaType = "Cheese"
        switch myModel.myColorString{
            case "Red": pizzaType = "Pepperoni"
            case "Green": pizzaType = "Veggie"
            case "Blue": pizzaType = "Gorgonzola"
            default: pizzaType = "Cheese"
        }
        let displayString = pizzaType + " Pizza!!"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

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

}

ColorsModalInterfaceController.swift

//
//  ColorsModalInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol ColorModalDelegate{
    
    func colorDidFinish(context:AnyObject?)
}

class ColorsModalInterfaceController: WKInterfaceController {

    //MARK: Properties
    var myModel = DemoModel()
    var delegate:ColorModalDelegate! = nil
    //MARK: - Action Methods
    @IBAction func redButtonPressed() {
        updateModel("Red")
        
    }
    @IBAction func greenButtonPressed() {
        updateModel("Green")
        
    }
    @IBAction func blueButtonPressed() {
        updateModel("Blue")
        
    }
    //MARK: - Instance Methods
    func updateModel(color:String){
        myModel.colorFromString(color)
        let newContext:AnyObject? = myModel
        delegate.colorDidFinish(newContext)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel //make the model
        delegate = myModel.delegate as? ColorModalDelegate //unbundle the delegate
        
    }

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

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

}

NumberInterfaceController.swift

//
//  NumberInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/5/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol NumberInterfaceDelegate{
    func numberDidFinish(context:Float)
}


class NumberInterfaceController: WKInterfaceController {
    
    //MARK: Outlets and properties
    var number:Float = 0.0
    var delegate:NumberInterfaceDelegate! = nil
    
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    //MARK: - Actions
    @IBAction func sliderDidChange(value: Float) {
        number = value
        updateDisplay()
    }
    
    @IBAction func doneButtonPressed() {
        delegate.numberDidFinish(number)
    }
    //MARK: - Instance Methods
    func updateDisplay(){
        let displayString = String(format:"%0.1f",number)
        statusLabel.setText(displayString)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        // unwrap the context
        let myModel = context as! DemoModel //make the model
        number = myModel.myNumber
        delegate = myModel.delegate as? NumberInterfaceDelegate
        updateDisplay()
    }

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

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

}

Swift WatchKit: Working with Modal Views Part 2: Presenting Programmatically

Photo Jun 10, 8 54 07 AMIn the first part in this series we implemented a modal interface in WatchKit with a segue. In this part we’ll present the modal programmatically and once again set up a delegate and context for moving data between controllers.

Open the project from the last lesson.  Press Command-N and add a new Cocoa Touch class  named NumberInterfaceController. Subclass WKInterfaceController, and make sure you set it to the Watchkit Extension Group.

Go to the Watchkit App storyboard. Drag a new  Interface Controller from the object library to the storyboard. Select the interface. In the identity inspector set the class to NumberInterfaceController.  In the attributes inspector, set the Identifier and Name to Numbers.

Drag a label, slider and button to the interface. Select the Label. Position the label Vertical: Top and Horizontal: Left. Set the width of the label to  Relative to Container.  Change the font to System 32 point centered.

Select the slider.  Position the Slider Horizontal: left, and Vertical: Center. Set the minimum to 0, maximum to 10 and steps to 20.

Select the button. Label the Button Done. Change the button background color to green (#00ff00 alpha 0.73).  Position the button Horizontal: Left and Vertical: Bottom. Your finished interface should look like this:

2015-06-10_08-49-29

Open  the assistant editor. Select the Number button on the I am Root interface. Control-drag from the button to the code. Add an action named numberButtonPressed and a //MARK: comment above it

//MARK: - Actions
@IBAction func numberButtonPressed() {
}

Select the Number interface. Control-drag from the label to the code. Make a property statusLabel. Control-drag from the slider to the code. Make an action sliderDidChange. Control-drag from the Done Button to the code. Make an action doneButtonPressed. Add the appropriate //MARK: tag. You should now have the following in your code:

//MARK: Outlets and Properties
@IBOutlet weak var statusLabel: WKInterfaceLabel!

//MARK: - Actions
@IBAction func sliderDidChange(value: Float) {
}

@IBAction func doneButtonPressed() {
}

We are now ready to code.

Presenting the Modal Controller

Presenting modal controllers in WatchKit is pretty similar to presenting controllers in UIKit, with two exceptions. First, we do not have any choices about animation in WatchKit. Secondly we pass as a parameter our context, as we have with the hierarchical controllers.

Close the assistant editor.  Go to the InterfaceController.swift code.    Change the code to this:

@IBAction func numberButtonPressed() {
    presentControllerWithName("Number", context: nil)
}

The method presentControllerWithName takes our destination controller’s storyboard ID and a context for parameters. For the moment we set the context to nil.

Build and run.  You should be able to present the Number interface when you press the number button.

2015-06-05_08-03-53

Adding the Context

As in the first part of the modal interface lesson,  we’ll use the class context we made to transfer data to the modal controller. Change the numberButtonPressed code to this:

@IBAction func numberButtonPressed() {
    myModel.myNumber  = 3.14 //test data
    //prepare the context
    let context: AnyObject? = myModel
    //present the controller
    presentControllerWithName("Number", context: context)
}

We added the context as AnyObject?, then present it to the controller. We also included some test data of 3.14.

Go to the NumberInterfaceController. Above the outlet, Add the following code to add the number property.

var number:Float = 0.0

Add the following code to awakeWithContext:

//MARK: - Life Cycle
override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    // unwrap the context
    let myModel = context as! DemoModel //make the model
    number = myModel.myNumber //grab the data from the model.
    updateDisplay()
}

When we awake the controller, we pass it our model. The method first casts the AnyObject? context back to a usable model, then we grab the number in the model to make it the number in the display. Once we have number, we update the display.

We’ll need an updateDisplay method. Add the following below the action methods:

//MARK: - Instance Methods
func updateDisplay(){
    let displayString = String(format:"%0.1f",number)
    statusLabel.setText(displayString)
}

Build and run. When we press the Number button, we now get 3.1 in the display.

2015-06-10_08-52-12

Add the Delegate

It’s nice to be able to send data to a view, but even more important to send it back with a delegate. Start making your delegate by adding the following above the NumberInterfaceController class declaration:

protocol NumberInterfaceDelegate{
    func numberDidFinish(context:Float)
}

Unlike the delegate we did last lesson, we made this one simpler. Instead of passing the model back to InterfaceController, we’ll pass just the number. For the color, we passed both a String and a UIColor to describe a color, we needed a class. This time, the only important data is a single Float, which simplifies everything.

Add a delegate declaration to the NumberInterfaceController under the declaration for number.

var delegate:NumberInterfaceDelegate! = nil

Change doneButtonPressed to this to call the delegate method

@IBAction func doneButtonPressed() {
    delegate.numberDidFinish(number)
}

Go to the InterfaceController.swift file. In the class declaration, adopt the protocol.

class InterfaceController: WKInterfaceController,ColorModalDelegate, NumberInterfaceDelegate {

Xcode will give you an error that the delegate isn’t implemented.  Add the following method under the colorDidFinish method

func numberDidFinish(context:Float){
    myModel.myNumber = context
    updateDisplay()
    dismissController()
}

Here’s where sending back a simple float pays off.  With a simple type, we do not have to cast when we assign it back to myModel.mynumber. We update the display and dismiss the  modal controller.

We’ll need to assign the delegate to myModel to pass it to the destination controller.  Add  myModel.delegate = self to numberButtonPressed. While there, comment out the test data line.


@IBAction func numberButtonPressed() {
    //let myModel.number = 3.14       //test data
    myModel.delegate = self
    //prepare the context
    let context: AnyObject? = myModel
    //present the controller
    presentControllerWithName("Number", context: context)
}

We also have to assign the delegate in NumberInterfaceController to the delegate property there. Go the awakeWithContext method in NumberInterfaceController and add delegate = myModel.delegate as? NumberInterfaceDelegate  to make it look like this:

//MARK: - Life Cycle
override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    // unwrap the context
    let myModel = context as! DemoModel //make the model
    number = myModel.myNumber //transfer the number
    delegate = myModel.delegate as? NumberInterfaceDelegate //transfer the delegate
    updateDisplay()
}

Build and run. You will be able to change the slider, press Done and the number will appear in the root display.

2015-06-10_08-59-42     2015-06-10_09-04-19

We’ve posted a single modal interface to the watch. There is one more type of modal interface: A page based one. In the conclusion to our lessons in modal interfaces, we’ll work with the page based interface.

The Whole Code

Storyboard

2015-06-10_08-50-18

InterfaceController.Swift

//
//  InterfaceController.swift
//  SwiftWatchModalDemo WatchKit Extension
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController,ColorModalDelegate, NumberInterfaceDelegate {
    
    //MARK: Outlets and Properties
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    @IBOutlet weak var colorButton: WKInterfaceButton!
    var myModel = DemoModel()
    
    //MARK: - Actions
    
    @IBAction func numberButtonPressed() {
        myModel.delegate = self
        //prepare the context
        let context: AnyObject? = myModel
        //present the controller
        presentControllerWithName("Number", context: context)
    }
    
    //MARK: - Delegates
    func colorDidFinish(context: AnyObject?) {
        myModel = context as! DemoModel
        dismissController()
        updateDisplay()
    }
    
    func numberDidFinish(context:Float){
        myModel.myNumber = context
        updateDisplay()
        dismissController()
    }
    
    //MARK: - Instance Methods
    func updateDisplay(){
        let statusText = String(format: "%1.2f", myModel.myNumber)
        statusLabel.setText(statusText)
        colorButton.setBackgroundColor(myModel.myColor)
    }
    
    //MARK: - Life Cycle
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        if segueIdentifier == "ColorsModal" {
            myModel.delegate = self
            return myModel
        }
        return nil
    }
    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()
        updateDisplay()
    }

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

}

NumberInterfaceController.swift

//
//  NumberInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/5/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol NumberInterfaceDelegate{
    func numberDidFinish(context:Float)
}


class NumberInterfaceController: WKInterfaceController {
    
    //MARK: Outlets and properties
    var number:Float = 0.0
    var delegate:NumberInterfaceDelegate! = nil
    
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    //MARK: - Actions
    @IBAction func sliderDidChange(value: Float) {
        number = value
        updateDisplay()
    }
    
    @IBAction func doneButtonPressed() {
        delegate.numberDidFinish(number)
    }
    //MARK: - Instance Methods
    func updateDisplay(){
        let displayString = String(format:"%0.1f",number)
        statusLabel.setText(displayString)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        // unwrap the context
        let myModel = context as! DemoModel //make the model
        number = myModel.myNumber
        delegate = myModel.delegate as? NumberInterfaceDelegate
        updateDisplay()
    }

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

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

}

DemoModel.Swift

//
//  DemoModel.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class DemoModel:NSObject{
    var myNumber:Float = 0.0
    var myColorString = "Black"
    var myColor = UIColor(white: 0.2, alpha: 0.73)
    var delegate:AnyObject? = nil
    
    func colorFromString(colorString:String){
        myColorString = colorString
        switch colorString {
        case "Red": myColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.73)
        case "Green":myColor = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.73)
        case "Blue":myColor = UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.73)
        default: myColor = UIColor(white: 0.2, alpha: 0.73)
        myColorString = "Black"
        }
    }
}

ColorsModalController.swift

//
//  ColorsModalInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol ColorModalDelegate{

    func colorDidFinish(context:AnyObject?)
}

class ColorsModalInterfaceController: WKInterfaceController {

    //MARK: Properties
    var myModel = DemoModel()
    var delegate:ColorModalDelegate! = nil
    //MARK: - Action Methods
    @IBAction func redButtonPressed() {
        updateModel("Red")

    }
    @IBAction func greenButtonPressed() {
        updateModel("Green")

    }
    @IBAction func blueButtonPressed() {
        updateModel("Blue")

    }
    //MARK: - Instance Methods
    func updateModel(color:String){
        myModel.colorFromString(color)
        let newContext:AnyObject? = myModel
        delegate.colorDidFinish(newContext)
    }

    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel //make the model
        delegate = myModel.delegate as? ColorModalDelegate //unbundle the delegate

    }

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

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

}

Swift Watchkit: Working with Modal Views Part 1: Segue with a Delegate

2015-06-02_06-08-04

Modal views on iPhones and ipads are used for input of information that requires attention. One of the on the Apple Watch is a modal view. You cannot mix a page-based interface with a hierarchical (i.e. navigation) interface as we learned in previous lessons. You can use modal interfaces with either.  In this series of lessons we’ll look at using the modal view.

Start by creating a new single-view project in XCode called SwiftWatchModalDemo. Use Swift as the language.  Once loaded select Editor>Add Target… from the drop down menu.  Add a Swift WatchKit Extension without a notification or glance.

Make a Model for the Demo

We’ll use a model for our demo that has a number and some color information in it, plus carry our delegate through the context. For strict MVC,  the delegate does not belong in a model. Note this model is really a context model for passing between controllers. If I had a real model class, I would have a class for passing controllers contain an instance of my data model and the delegate or I’d use a dictionary or struct for the context.   This delegate cannot do anything in the model in its current state, so this is not as much of a break from MVC as it appears.

Press Command-N to make a new Cocoa Touch Class. Make a class DemoModel, subclassing NSObject. Make sure to set group to WatchKit Extension. Add the following code:

class DemoModel:NSObject{
    var myNumber:Float = 0.0
    var myColorString = "Black"
    var myColor = UIColor(white: 0.2, alpha: 0.73)
    var delegate:AnyObject? = nil

    func colorFromString(colorString:String){
        myColorString = colorString
        switch colorString {
            case "Red": myColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.73)
            case "Green":myColor = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.73)
            case "Blue":myColor = UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 0.73)
        default: myColor = UIColor(white: 0.2, alpha: 0.73)
            myColorString = "Black"
        }
    }
}

We have a Float , a String for a color name, and a UIColor  as data. We also have delegate as AnyObject? As we discussed with delegates for the navigation interface, we’ll downcast the delegate in  the awakeFromContext method of the destination controller.

I added a method to convert a string to a color with an alpha value of 0.73, which is the default alpha of a button in WatchKit. If the color does not match my list, I make it black.

WatchKit Modal Interfaces by Segue

Set up the Storyboard

Once we have our model, set up our view.  Go to  the Interface storyboard of the WatchKit app.  Add  a label and two buttons in that order. Leave them in their default positioning.  Select the interface. In the attributes inspector, set the Identifier to IAmRoot and the Name to I am Root.  Set the Width of the Label to Relative to Container and center justify the label. Set the title of one button to Color and the other button to Number like this.

2015-06-01_05-52-05

Add another interface to the storyboard by dragging from the object library. Add three buttons. Title the interface Colors and set the Interface’s ID to ColorsModal.  Set the title of the top button to Red and set the back of this button to red(#FF0000 alpha 0.73).  Do the same for the two other buttons, labeling and coloring them green(#00FF00 alpha 0.73) and blue(#0000FF alpha: 0.73).

2015-06-01_05-53-30

Control-Drag from the Color Button to the Colors interface.

2015-06-02_06-08-04

Release the mouse button. You will get a choice of segues. Select modal.

2015-06-02_06-08-39

Select the segue that appears and label it  ColorsModal

2015-06-02_06-11-32

Build and run. You will be able to press the Colors button and the modal will slide up to show the colors modal. You can tell a modal interface because they slide vertically. To dismiss, tap the colors button.

2015-06-03_06-13-02

Note that modals do not have the time in the navigation bar like the  controller  in Root does.

Wire Up the Interface

Let’s make this do something useful. We will change the color of the color button in the root interface. Press Command-N to make a new Cocoa Touch Class. Make a class ColorsModalInterfaceController. Subclass WKInterfaceController and make sure it is in the WatchKit Extension group before pressing Create in the file window.  Once it loads, go back to the storyboard.  select the Colors Interface and in the indentity inspector set the class to ColorsModalInterfaceController.

Open the assistant editor.  Select the I am Root interface. Control-drag from the label to the code. Make an outlet named statusLabel. Control-drag from the Color Button to the code. Make an outlet named colorButton.  Under the outlets, add var myModel = DemoModel() to make an instance of our model.  Your code should now have this:

//MARK: Outlets and Properties

@IBOutlet weak var colorButton: WKInterfaceButton!
@IBOutlet weak var statusLabel: WKInterfaceLabel!
var myModel = DemoModel()

Select the Colors interface. Control-drag from the Red button and make an action redButtonPressed. Do the same for the Green and Blue buttons. You should have this in your code when done.

    //MARK: - Action Methods
   @IBAction func redButtonPressed() {

    }
    @IBAction func greenButtonPressed() {

    }
     @IBAction func blueButtonPressed() {

    }

Set up the Colors Interface for a Delegate

For this modal, we will select a color, then change the background color of the button in the root interface using a delegate. In the code we just added ,change it to this:

    //MARK: - Action Methods
   @IBAction func RedButtonPressed() {
       updateModel("Red")
    }
    @IBAction func GreenButtonPressed() {
       updateModel("Green")
    }
     @IBAction func BlueButtonPressed() {
       updateModel("Blue")
    }

We added a method updateModel. In UIKit, I would have taken the titleLabel property and would have written one action method for all three buttons. In WatchKit, we have no access to the properties. Instead, make a method that does the same, and change a parameter which has that title explicitly in it. Now add the updateModel method:

    //MARK: - Instance Methods
    func updateModel(color:String){
        myModel.colorFromString(color)
        let newContext:AnyObject? = myModel
        delegate.colorDidFinish(newContext)
    }

The model takes the string and updates the model with the string and color information. We then create a new context using the updated model, and then use newContext as the parameter of our delegate.

As we can tell from the errors, we need to set up the model, delegate and the protocol. Add this to your code, above the actions:

//MARK: Properties
    var myModel = DemoModel()
    var delegate:ColorModalDelegate! = nil

To make the delegate work, we need a protocol. Add the protocol between the ColorModalInterfaceController class definition and below the import Foundation lines:

protocol ColorModalDelegate{

    func colorDidFinish(context:AnyObject?)
}

We need to pass the values from the InterfaceController to this controller. Change the awakeWithContext method to this:

    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
// Configure interface objects here.
        myModel = context as! DemoModel //make the model
        delegate = myModel.delegate as? ColorModalDelegate //unbundle the delegate

    }

There are two steps here. First we take the context and assign it to a model, downcasting AnyObject? to DemoModel. Then we take  myModel and assign the delegate property to the class’ delegate, downcasting AnyObject? to the protocol ColorModalDelegate.

Prepare the Segue

We will need to send data to the segue in the root.
Modal and Navigation controllers in WatchKit act alike in many ways. To pass data through a segue we override the same method contextForSegueWithIdentifier(segueIdentifier: String) which returns the context for the destination controller, much like prepareForSegue does in UIKit. Go to the InterfaceController class. Add the following code.

    //MARK: - Life Cycle
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        if segueIdentifier == "ColorsModal" {
            myModel.delegate = self

            return myModel
        }
        return nil
    }

If the user presses the Colors button and activates the ColorsModal segue, we assign values to the context. In this case we prepare the delegate with myModel.delegate = self.

Adopt the Protocol

Adopt the protocol by changing the class definition of InterfaceController to this:

class InterfaceController: WKInterfaceController,ColorModalDelegate {

Now add the required method for the protocol:

    //MARK: - Delegates
    func colorDidFinish(context: AnyObject?) {
        myModel = context as! DemoModel
        dismissController()
        updateDisplay()
    }

We change the context to the model for this controller, then dismiss the controller using the method dismissController. Unlike its UIKit counterpart, you have no control over animation, so this method is a lot simpler. We finish our delegate method by updating the display. We could have put colorButton.setBackgroundColor(myModel.color)in our code, but as we expand the application in the next lessons, you will find we end up repeating code. Add the following to the InterfaceController code:

    //MARK: - Instance Methods
    func updateDisplay(){
        let statusText = String(format: "%1.2f", myModel.myNumber)
        statusLabel.setText(statusText)
        colorButton.setBackgroundColor(myModel.myColor)
    }

The other place that we can add updateDisplay is the willActivate method, which is the viewWillAppear of WatchKit.

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

    }

Build and Run. Tap the Colors button. Tap the Red Button. The Colors button is now red:

2015-06-03_06-45-25

We now have a working modal from a segue. In the next part, we’ll present modals programmatically.

The Whole Code

InterfaceController.swift

//
//  InterfaceController.swift
//  SwiftWatchModalDemo WatchKit Extension
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController,ColorModalDelegate {

    //MARK: Outlets and Properties
    @IBOutlet weak var statusLabel: WKInterfaceLabel!

    @IBOutlet weak var colorButton: WKInterfaceButton!
    var myModel = DemoModel()

    //MARK: - Delegates
    func colorDidFinish(context: AnyObject?) {
        myModel = context as! DemoModel
        dismissController()
        updateDisplay()
    }
    //MARK: - Instance Methods
    func updateDisplay(){
        let statusText = String(format: "%1.2f", myModel.myNumber)
        statusLabel.setText(statusText)
        colorButton.setBackgroundColor(myModel.myColor)
    }

    //MARK: - Life Cycle
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        if segueIdentifier == "ColorsModal" {
            myModel.delegate = self

            return myModel
        }
        return nil
    }
    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()
        updateDisplay()
    }

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

}

ColorsModalController.swift

//
//  ColorsModalInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol ColorModalDelegate{

    func colorDidFinish(context:AnyObject?)
}

class ColorsModalInterfaceController: WKInterfaceController {

    //MARK: Properties
    var myModel = DemoModel()
    var delegate:ColorModalDelegate! = nil
    //MARK: - Action Methods
    @IBAction func redButtonPressed() {
        updateModel("Red")

    }
    @IBAction func greenButtonPressed() {
        updateModel("Green")

    }
    @IBAction func blueButtonPressed() {
        updateModel("Blue")

    }
    //MARK: - Instance Methods
    func updateModel(color:String){
        myModel.colorFromString(color)
        let newContext:AnyObject? = myModel
        delegate.colorDidFinish(newContext)
    }

    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel //make the model
        delegate = myModel.delegate as? ColorModalDelegate //unbundle the delegate

    }

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

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

}