Tag Archives: Apple Watch

Why do we need Delegates in iOS and WatchOS?

About two years ago someone asked me a very good question: Why do we need delegates for UIViewControllers?  He thought Swift  made things easier, but this delegate stuff seems very complicated. Shouldn’t we be able to send a message or initializer between classes?

When I first learned iOS, I’ll admit it took me months to understand what happened with delegation. I found lots of confusing code and little explanation. AS I was researching some more preferences to send people to, I found the results lacking. Most often, tutorials refer to how to use an Apple factory delegate, not making your own callback. Its these callbacks which require full knowledge of delegates.

I decided it was time to update this, and to include two examples developers might run into: the iOS and watchOS versions. With the maturing of watchOS in watchOS 3 I think more developers might begin to look at developing watch apps, and there’s some twists there that might cause some confusion.

Let’s start at the beginning, so everyone understands the problem.

What is a Class?

Let’s start at the beginning, so everyone understands the problem. While we use classes in object-oriented programming, it’s good to review what they actually are. A class is a collection of data, which we call properties, and actions we can do to those properties, which we call methods.

Properties and methods are either public or private. A public method is one that classes other than the defining class can see and can use. Private means that the property or method is only usable and visible within the defining class. Other classes cannot see or use it. In Swift the private keyword makes properties and methods private. Swift’s calculated properties feature is another way to make properties private. In Swift, there is also a default state which makes a method or class public to the current target, but not other targets.

We tend not to like other people messing with our insides, and that is a good programming practice too. In general, it is best to leave what is public by other classes to a necessary minimum. Keeping properties and methods private and not exposing all of our class is known as encapsulation.

Encapsulation allows us to make code as modular as building blocks. Just as a few stubs come out of an otherwise standard sized brick, only a few usable methods come out of a class. Then they can attach to a lot of other bricks.

What is  Model –  View – Controller  or MVC?

MVC schematic blankA term heard often  when working with Xcode, is MVC. MVC stands for Model-View-Controller. It is not an exclusive term to Xcode projects. It is a pattern of programming, a good organization of any program or application in a graphics-rich environment, and arguably any environment that interacts with the user. MVC separates the major parts of an application. First it separates the data and the user interaction then adds an intermediary between them. Why is this important? You might write and publish an application for an iPhone, then decide an iPad version would be a good idea, then decide to make a watch version. With MVC, you only change one part completely, the view and possibly some of the controller. The code handling your data never changes between the versions saving a lot of time and effort.

What is a Model?

MVC schematic modelThere are parts of our program that deal with information we want to process. A pizza ordering system has a list of data giving us information about each person’s order. There may be other links to that data with more data about each customer, and about each pizza. In a pizza ordering system this is our model: the collection of all the data we will use in our ordering system. It does not in any way interact with the user. It does not display anything or does it ask for input. It is just data. Here is an example of very simple model:

class switchState{
    var state:Bool
    func textState()->String{
        if state {
            return "On"
        } 
        return "Off"
    }
    func overLoadSwitch(users:Int,load:Int){
      let totalLoad = users * load 
      if totalLoad > 100{
          switch = false
      }
}

This model is the state of a switch.  That’s the data. the model has two methods. textState(), describes the state of the switch as a string,  overLoadSwitch() turns off the switch if users multiplied by load is greater than 100 . There is a lot more methods I should add to describe the switch, but any method is changing or describing data only. There is no user input or output here. Models might make calculations but again there is no user interaction here.

What is a View?

MVC schematic viewWhere all the user interaction happens is in the view. In Xcode, most people use Interface Builder either as a scene in a storyboard or a .xib file to build their views.  A developer can programmatically create a view class to hold the different controls.

2016-08-01_07-11-48As the model never interacts with the user,  the view never interacts directly with the data. The view doesn’t do much but sit there. It might respond to a user touch with feedback such as a notifying a method somewhere, a color change when a button gets tapped or a scrolling motion at times, but that is all it does. The view does contain a lot of properties and methods and that tell us the state of the view. We can change the appearance and behavior of the view through methods and properties. The view can tell the controller that there was a change in the view, such as a button getting pressed, or a character typed. it can’t do anything about it, but it can broadcast something.

What is a Controller?

MVC schematicThe heart of MVC connects these two. Called the controller or view controller, it coordinates what happens in the model and what happens in the view. If a user presses a button on the view, the controller responds to that event. If that response means sending messages to the model, the view controller does that. If the response requires getting information from the model, the controller does that too. in Xcode, @IBOutlet and @IBAction connect Interface Builder files containing views to the view controller.

The key to MVC is communication. To be more accurate, the lack of communication. MVC takes encapsulation very seriously.  The view and the model never directly talk to each other. The controller can send messages to the view and the controller. The view and controller may do an internal action to the message sent as a method call or it may return a value to the controller. The controller never directly changes anything in either the view or the model.

So to summarize, a view, a model and a controller cannot directly change a property in each other. A view and a model may not talk to each other at all. A view can tell the controller there is a change in the model. A controller can send messages in the form of method calls to the view and the model and get the responses back through that method.

MVC schematic full annotated

How Does MVC Connect to Other MVC’s

What we’ve discussed so far is for only one scene in a much larger application. Suppose I have the following  watchOS storyboard:

2016-09-21_05-01-58

I have an a button Switch which loads a second face that has a switch.  When I decide which way I want the switch, I press done.

The Easy Direction

In Xcode we have what are known as segues. Segues are a convenience to point from one view controller to another.  When segues keep track of some things that would become cumbersome to control ourselves in a simple way. When we move from one controller to the next, the segue tells the system to open this particular view controller, which then opens up a view and model. The model and view in the new MVC setup is different then the calling one. Apple includes a method prepare(for segue:) for  iOS which give us a chance to set values in the new view controller,  and subsequently the new view controller’s view and model.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "switch"{
         let vc = segue.destination as! SwitchViewController
         vc.switchState = false
        }

With the introduction of WatchOS came a slightly different approach.  instead of having the full model available,  watchOS sends to new controllers a single value called context. As this is type Any?,  you can put whatever you want in this value. Most often developers send dictionaries of values to other controllers through the contextForSegue method. When the destination application wakes up. the awake method  converts the context to the proper type and assigns it correctly.

override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
        return self
    }

For both a identical  phone or watch app I could press the Switch button.  It launches  the switch face and  pass a value to the switch to be false.

The Problem Direction

We can turn the switch off and on easily enough.  But when we press Done to send it back to the original controller  is when problems show up. By the rules of MVC, we need a method to return a value. Where in a called instance can we go back to the class calling it? With an encapsulated class we can’t. There is no way to send that revised model back to the original controller without breaking encapsulation or MVC. The new view controller does not know anything about the class that called it. We look stuck. If we try to make a reference directly to the calling controller, we may cause a reference loop that will kill our memory. Simply put, we can’t send things backwards.

This is the problem that delegates and protocols solve by being a little sneaky. Imagine another class, one that is really a skeleton of a class. This class contains only methods. It declares that certain methods are in this class, but never implements them. In Swift they are protocols. We make a protocol class that has one method. That method is what you do when you are done  with the switch, and want to go back to the calling controller. It has a few parameters, things you want to pass back to the calling view controller.  So it might look like this:

protocol SwitchDelegate {
    func didFinishSwitch(switchState:Bool)
}

I passed back the state of the switch in this case.

In the controller with the switch, we make an instance of this protocol, calling it delegate.

delegate:SwitchDelegate! = nil

Since we have a property of type SwitchDelegate, we can use the methods of the SwitchDelegate type, In our example, that is our method didFinishSwitch. We can stick that method call in a action for a Done button:

@IBAction func doneButtonPressed(sender:UIButton!){
    delegate.didFinishSwitch(switchState: switchState)
    dismiss(animated: true, completion: nil)
}

or for WatchOS

@IBAction func submitSwitchStatus() {
    delegate.didFinishSwitch(switchState: switchState)
    pop()      
}

Since protocols are skeletons, it means any other class can adopt them. A class makes the protocol methods part of its own class with a stipulation. As soon as a protocol gets adopted, you need to flesh out the skeleton. The developer has to code the required methods in the adopted class’ code. We adopt a protocol by placing it after the name of the class and superclass. For iOS you might have

class OrderPizzaViewController:UIViewController,PizzaEditDelegate

and for watchOS, you might have

class InterfaceController: WKInterfaceController,SwitchDelegate {

As soon as you do that, you will get a compiler error since the protocol’s method does not exist in the class. In the code for the adopting class, in our example OrderPizzaViewController, we would implement the method

func didFinishSwitch(switchState: Bool) {
        if switchState {
            textState = "Switch is On"
        } else {
            textState = "Switch is Off"
        }        
    }

We get the data back, and what we need with it, in this case a string that we’ll print to the label.
One more step. While back in the destination controller, I said the delegate was an instance of the protocol, I didn’t say where the delegate was. In prepare(for Segue) I add one more line vc.delegate = self saying the protocol is your controller

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "switch"{
         let vc = segue.destination as! SwitchViewController
         vc.switchState = false
         vc.delegate = self
        }

In WatchOS this gets a bit trickier. I have only one context to pass both the switchState and the delegate. For multiple values generally developers use a dictionary like this:

override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
        let context:[String:Any] = ["switchState":false,"delegate":self]
        return context
    }

the awake method would have code to unwrap the dictionary and assign the values.

When we tap Done in a watch or phone application, the method runs, it knows it is located in the calling controller, and calls it there where we added to the original class.The data is a parameter so the program can easily transfer into the controller and to the model. Delegates and protocols are bit sneaky but it works, and is one of the most important techniques when working with view controllers.

mvc-schematic

The original question asked why didn’t Swift make this simpler. As I hope I’ve shown here with a Swift context, it doesn’t matter what object oriented language you use. Delegates are part of the MVC pattern, which is a good programming practice.

Make a WatchOS 3 Haptic Catalog with a Picker

How does the Apple Watch communicate to the user when they are not looking at the watch face? That is done with haptics. Haptics are sounds and taps letting the user know something is happening. In this lesson, I’ll explain to you how to use haptics in Watch OS applications by creating a catalog of all the haptics using a WatchOS picker.

The picker control is an object for selection of multiple items. You can use the picker to display either full-screen images or text for selection. In this lesson, I’ll keep it simple with a text-based picker.

Make a New Project

Create a new WatchOS project called HapticPickerDemo. Use Swift as the language, uncheck Notifications, and keep the device Universal.  Open the interface.storyboard file in the WatchKit App group.

Find the picker in the object library. Drag a picker to the interface.

2016-08-27_07-01-54

With the picker selected you’ll see four attributes of a
picker.  The Focus Style and Indicator give you visual elements to highlight the picker. The most important attributes are Style and of course Enabled. You have three style choices: List, Stack and Sequence. List is a text-based picker. Stack and Sequence is an animated and non-animated image picker.

2016-08-27_07-09-18

Set your attributes to the illustration above.  Set the picker’s Style  to List. Set the Focus Style to  Outline with Caption. This will show an outline around the picker with a caption at the top. This caption is context sensitive to the picker selection. If you’ve set a complication on a watch, you are familiar with this focus style, such as  the black on green lettering  for weather in this setting:

2016-08-27_07-02-50

From the Object Library, add  a button under the picker. Title the button Play Haptic.

2016-08-27_07-04-29

Close the attributes and navigation panels. Open the assistant editor.

Make an outlet for the picker by control dragging from the picker to the InterfaceController class

@IBOutlet var picker: WKInterfacePicker!

Make an action for the picker’s action,changing Outlet to Action in the popup after you control drag.

 func pickerDidChange(_ value: Int) {
    }

Make an action for the button’s action, changing Outlet to Action in the popup after you control-drag.

 func playHaptic(_ value: Int) {
    }

Playing a Haptic

There’s two parts to a haptic you need to know. There is a play function [in WatchOS 2 playHaptic()]  you call on the current device object called by
WKInterfacedevice.current()object [in WatchOS 2 WKInterfaceDevice.currentdevice() ]. The play method has one parameter of type WKHapticType, which is an enumeration of haptic types.

  • notification tells the user there is a notification,
  • directionUp indicates an upward value,
  • directionDown indicates a downward value,
  • success indicates the successful completion of a task,
  • failure a failed task,
  • retry tells the user to retry,
  • start the beginning of an action,
  • stop the end of an action,
  • click is a very slight click, which you probably won’t hear but probably feel on a watch.

Close the Assistant editor.  Go to InterfaceController.swift in the App extension group. To make the button play a .success haptic change the playHaptic action to this:

 
func playHaptic(_ value: Int) {
    WKInterfaceDevice.current().play(.success)
}

Build and run using the 38mm simulator. Press the Play Haptic Button and  you get a sound.

In the simulator, you don’t get the taps you’ll get if you were using the watch. If you have a watch, run this on your watch and you’ll feel the tap.

Implementing the Picker

There is no documentation on what the taps feel like. It’s hard to describe in words,you have to feel it. In the rest of this lesson we’ll use WatchKit’s WKInterfacePicker to make a selectable catalog of haptics you can run on an Apple Watch.

Pickers have one parameter. Since pickers hold the list in a sequence, the parameter value is the index of that sequence. Delete all life cycle methods but willActivate. Add the list of haptic types exactly in the order above for the picker as an array.

let titles = [
    "notification","directionUp",
    "directionDown","success",
    "failure","retry",
    "start","stop","click"
]

Pickers will not use these arrays directly. Pickers get their selections from a WKPickerItem object. Code will create an array of WKPickerItem, and set that as the picker’s items. WKPickerItem has several properties to make the picker flexible.

  • title – A String? to use in a list
  • caption – A String? used as a caption for item in a list
  • accessoryImage – A Small WKImage? to display next to title in a
    list or as an alternate to text as in small complication setting.
  • contentImage – In a Stacked or Image Sequence style, a
    WKImage?

The picker has a setItems method which takes the array of picker
items to make the list, stack, or sequence of images, depending on the display style. For every picker you create, you build a function that iterates through the arrays, adding the elementsts as pickerItems. Add a function refreshPickerItems:

 func refreshPickerItems(){
 }

Add an empty array of picker items to the function.

 
func refreshPickerItems(){
    var pickerItems:[WKPickerItem] = []
}

Add a loop iterating through the titles.

 
func refreshPickerItems(){
    var pickerItems:[WKPickerItem] = []
    for item in titles{
    }
}

In the loop creates an instance of WKPickerItem. Add to pickerItem the title  from the corresponding element of the arrays. Add the literal caption Haptic. (If you want to experiment, with captions, try coding it as "Haptic-" + item ) Add the picker item to the pickerItems array.

func refreshPickerItems(){ 
    var pickerItems:[WKPickerItem] = [] 
    for item in titles{ 
        let pickerItem = WKPickerItem()
        pickerItem.title = item
        pickerItem.caption = "Haptic"
        pickerItems += [pickerItem]
    }
}

Once the pickerItems array is complete, set the items in the picker.

func refreshPickerItems(){ 
    var pickerItems:[WKPickerItem] = [] 
    for item in titles{ 
        let pickerItem = WKPickerItem()
        pickerItem.title = item
        pickerItems += [pickerItem]
    }
    picker.setItems(pickerItems)
}

In willActivate, call this function to refresh the list.

override funcwillActivate() {
    super.willActivate()
    refreshPickerItems()
}

Selecting a Picker Entry

If you were to run the project now, you would see the picker displayed correctly. You could not select anything though. That happens in the action. The pickerDidChange action has a Int parameter value, which is the current index on the picker.

I told you earlier to be careful getting the order correct in the titles  array.  This is why. Since WKHapticType is a enum you can select the type by the rawValue of the enum. That raw value matches the value parameter. Before we try this, add a property hapticType to store the type.

varhapticType:WKHapticType = .notification

I’ve initially set this to .notification. In the pickerDidChange action set the hapticType property with the raw
value we can get the value parameter.

@IBAction func pickerDidChange(_ value: Int) { 
    hapticType = WKHapticType(rawValue:value)!
}

Playing the Haptic

The last step is to play the haptics. To the action  pickerDidChange  add a .click haptic

WKInterfaceDevice.current().play(.click)

This demonstrates one use for haptics, feedback to the user.  In the action playHaptic, change the .success haptic to the property hapticType

 @IBAction fund playHaptic() {
        WKInterfaceDevice.current().play(hapticType)
    }

Run in the 38mm simulator. Select a haptic and press Play Haptic. You’ll hear the chime for it.

If you have a watch, load it on your watch, and try the application. When you tap Play haptic on the watch you will get the watch tap as well.

Don’t over use Haptics. Unless your app is an interval timer, don’t  have one go off every second or minute. That just gets annoying. Use haptics at the right times to attract a user’s attention to their Apple watch or give instant feedback.

The Whole Code

//
//  InterfaceController.swift
//  HapticPickerDemo WatchKit Extension
//
//  Created by Steven Lipton on 8/26/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {

    @IBOutlet var picker: WKInterfacePicker!
    
    //List of WKHapticType in rawValue order
    let titles:[String] = [
        "notification","directionUp",
        "directionDown","success",
        "failure","retry",
        "start","stop","click"]
    
    var hapticType:WKHapticType = .notification
    
    //Use the index from the picker as the rawValue for the WKHapticType
    @IBAction func pickerDidChange(_ value: Int) {
        hapticType = WKHapticType(rawValue: value)!
        WKInterfaceDevice.current().play(.click)
    }
    
    
    @IBAction func playHaptic(){
        WKInterfaceDevice.current().play(hapticType)
    }
    
    
    func refreshPickerItems(){
        var pickerItems:[WKPickerItem] = []
        for item in titles{
            let pickerItem = WKPickerItem()
            pickerItem.title = item
            pickerItem.caption = "Haptic"
            pickerItems += [pickerItem]
        }
        picker.setItems(pickerItems)
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        refreshPickerItems()
       
        
    }

}

Accessing the Digital Crown in WatchOS

On the Apple Watch, the digital crown seems to be a great way to control your watch. the Slider and picker controls do use it, but direct developer use was prohibited in Watch OS2. In a nice change, Watch OS3 does. In this lesson, we’ll show how to get data from the digital crown in your applications.

Start a new Xcode WatchOS project DigitalCrownDemo. Deselect the notifications and save the project.

2016-08-22_06-26-21

Add three labels to the watch interface. Title them as follows:

2016-08-22_05-51-19

In the assistant editor, Connect the labels to outlets like this:

@IBOutlet var statusLabel: WKInterfaceLabel!
@IBOutlet var valueLabel: WKInterfaceLabel!
@IBOutlet var rpsLabel: WKInterfaceLabel!

We’ll track two data values the rotation speed and a value based on the change in value from the watch. Add them to the controller

var rps = 0.0
var value = 0.0

Add a update method for the display:

func updateLabels(){
    valueLabel.setText(String(format:"Value:%1.3f",value))
    rpsLabel.setText(String(format:"RPS:%1.3f rps",rps))
}

The crown uses a delegate. In interface controller, Adopt the <code>WKCrownDelegate</code>
class InterfaceController: WKInterfaceController,WKCrownDelegate {

The crown must have focus to return information. In awake(withContext) set the focus for the crown

    
override func awake(withContext context: AnyObject?) {
    super.awake(withContext: context)
    crownSequencer.focus()
    crownSequencer.delegate = self
}

The interface controller has a property crownSequencer that monitors the activity on the crown. We set the focus to crownSequencer and set the delegate method locations to this class.

Add the crownDidRotate delegate method, which returns values when there is a change in the rotation of the crown.

//MARK: Delegates
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
   value += rotationalDelta
   rps = (crownSequencer?.rotationsPerSecond)!
   statusLabel.setText("Moving")
   updateLabels()        
}

We have two arguments in this delegate method: the crownSequencer and the rotationalDelta. The Rotational debts is the change between the current and last position of the crown's rotation. One of the properties of the crownSequencer is rotations per second, giving a speed to the rotations. The code places the rotational delta and the rotations per send into a string for output to the watch. Most likely we'll want a position from the rotational delta, so we've added it to value.

Add the crownDidbecomeIdle delegate method

 
func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
    rps = (crownSequencer?.rotationsPerSecond)!
    statusLabel.setText("Stopped")
    updateLabels()
}

This delegate method fires when the crown stops. The code prints that the crown stopped and the last rotational speed before it did.

Run the code on the 42mm simulator. To use the crown hardware in the simulator, After a single click on the background of the watch face drag up and down two fingers. Stopping and starting the drag, you will see the display change.

The crownSequencer is smart enough to know your watches orientation from your settings. Up is always a positive value and down is always negative. there's a lot to do with this, from making new controls for your watch to an input for small games with sprite kit and scene kit.

The Whole Code

//
//  InterfaceController.swift
//  DigitalCrownDemo WatchKit Extension
//
//  Created by Steven Lipton on 8/22/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController, WKCrownDelegate {
    @IBOutlet var statusLabel: WKInterfaceLabel!
    @IBOutlet var valueLabel: WKInterfaceLabel!
    @IBOutlet var rpsLabel: WKInterfaceLabel!

    var value = 0.0
    var rps = 0.0
    
    func updateLabels(){
        valueLabel.setText(String(format:"Value:%1.3f",value))
        rpsLabel.setText(String(format:"RPS:%1.3f rps",rps))
    }
    
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
        crownSequencer.focus()
        crownSequencer.delegate = self
    }
    //MARK: Delegates
    func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
        value += rotationalDelta
        rps = (crownSequencer?.rotationsPerSecond)!
        statusLabel.setText("Moving")
        updateLabels()
        
    }
    
    func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
        rps = (crownSequencer?.rotationsPerSecond)!
        statusLabel.setText("Stopped")
        updateLabels()

    }

    
}

Adding Actions to iOS and WatchOS Local Notifications

Even if you never want to make an app for the Apple Watch, there’s one place you might want to think about supporting: Notifications. In a previous lesson, we explored how to make local notifications for iOS devices. That lesson ended with a free bonus: If a phone is sleeping and you have an Apple Watch, the notification will go to the watch.

Starting in iOS8, notifications get one big change: they can have actions of their own. On phone, tablet or watch this gives you more flexibility in how to respond to a notification. Suppose you had an app that after an appointment comes up, nags you about it every ten seconds until you stop the nagging. You could of course go into the app and shut it off, but it might be easier to have a stop nagging button on the notification. In this lesson, we’ll make that stop nagging button first for iOS devices, then for the Apple Watch. Along the way we’ll discover why watch notifications are the one place iOS developers should seriously think about for their phone apps.

Making a Demo Program – A Review of Notifications

In a previous lesson we went into detail about local notifications. We’ll be using a lot of what was learned there. In this tutorial, we’ll build a simple app to make a series of notifications similar to the HIIT app at the end of that lesson. We’ll use this as a review and quick introduction to local notifications if you are not familiar with them.

Since we will eventually get to some watch related notifications, make a new project using the iOS with Watch OS App template.

2016-04-26_15-32-23

Name the project PhoneWatchNotificationDemo and make sure watch notifications is checked on and everything else is off.

2016-04-26_15-32-22

Once you save the project, add a new file by pressing Command-N called NaggyNotification subclassing NSObject. Add these properties to the class

class NaggyNotification: NSObject {
    private let basicNags = ["Nag", "Nag nag","Naggity nag"]
    private let emojiNags = ["😀","😉","🙄","😒","😠","😡"]
    private var nags = ["Nag", "Nag nag","Naggity nag"]
    private var counter = 0
    private var timer = NSTimer()
    private var backgroundTask = 0
    private var localNotification = UILocalNotification()
    var nagging:Bool {return timer.valid } //read only
}

If you don’t know how to type emojis, press Control-Command-Spacebar to get the symbols palette.

We are going to do something insane a little later with this class. To protect against some of the problems with that, all but one of our properties is private and inaccessible outside the class. There is one read-only property nagging which uses a computed read-only value.

Most of these properties we’ll get to. We’ll start with the most important. The localNotification will contain our notification. Add the following method to present it immediately:

    func presentNotificationNow(){
        UIApplication.sharedApplication().presentLocalNotificationNow(localNotification)
    }

There are two ways to present notifications. We can present immediately as we do with the presentLocalNotificationNow method above or at some scheduled time in the future with the scheduleLocalNotification method. We could schedule that time using the fireDate property of UILocalNotification, but that presents a problem. We can only schedule 64 notifications, which in a nagging app might be too little space. We will take a different approach: Use a NSTimer loop in the background. Add the following method to start a timer:

func startTimer(timeInterval:NSTimeInterval){
        if !timer.valid{ //prevent more than one timer on the thread
            counter = 0
            backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
            timer = NSTimer.scheduledTimerWithTimeInterval(
                timeInterval,
                target: self,
                selector: #selector(timerDidFire),
                userInfo: nil,
                repeats: true)
        }
        
    }

We do three things in this code. We reset a counter for counting how many nags the user has been subjected to, then set this task into the background. While on a simulator you can leave things in the background without this, your app will suspend operation without beginBackgroundTaskWithExpirationHandler on a live device. Finally we start a timing loop, getting the timing interval from the parameter timeInterval. The timing loop ends on a selector timerDidFire. Let’s add the code for timerDidfire. Add this to the class:

    func timerDidFire(timer:NSTimer){
            counter += 1
            let currentNag = counter % nags.count //cycle through messages
            let nagString = String(format:"# %i %@",counter,nags[currentNag])
            localNotification.alertBody = nagString
            localNotification.alertTitle = "Naggy Notification"
            localNotification.category = "Nags"
            localNotification.soundName = UILocalNotificationDefaultSoundName
            localNotification.userInfo = ["NagNum":counter,"NagString":nags[currentNag]]
            presentNotificationNow()
    }

If you are not familiar with timing loops you might want to refer to this lesson. Basically, the timer object is set to run the timerDidFire function every timeInterval seconds. Once in the timerDidFire function, we increment counter and compose a string we’ll post to the notification. The property alertBody of UILocalNotification sets the text for the notification. We also set the sound to the default sound, which will also set off vibrations and haptics. For later use, we all send a dictionary containing our count and our current nag as userInfo we can use later in this lesson. Finally we present the notification.

We’ll also need a way to shut down the timer. Add this method to your code:

func stopTimer(){
        timer.invalidate()
        UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
}

We shut down the timer and remove the task from the background.

Now for the small, but insane bit of code. Add this above the class declaration:

let naggingNotification = NaggyNotification()

class NaggyNotification: NSObject {
...

A variable declared outside a class is global. Global values or instances can be used anywhere in the target code. There are other, better ways of handling the NaggyNotification class, but I could write full tutorials on them. As we’ll see, this instance will show up in several classes. This is a simple way to get it into all of them. As I always do when I use a global, I warn people not to use globals except in special circumstances. They are notoriously hard to track down in code. It’s why I made all the properties private, only methods are accessible to other classes, which are easier to deal with.

In order to use a notification, you must tell the application you will be using the notification in the AppDelegate.swift file. Go to the AppDelegate class Change the application:didFinishLaunchingWithOptions:launchOptions: method to this:

 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(
                forTypes:[.Alert, .Sound],
                categories: nil
            )
        )
        return true
    }

We register the notification settings for the two types of notifications: Alerts(or banners) and sounds(or haptics and vibrations).

Our app has all the internal parts set, we need a user interface. We’ll make a very simple one of a single button. Go to the ViewController.swift file. Change ViewController to this:

class ViewController: UIViewController {  
    @IBOutlet weak var nagButton: UIButton!
    @IBAction func nagButton(sender: UIButton) {
    }
 }

We’ll have one button that we’ll assign a outlet and action to. Change the nagButton action to this:

@IBAction func nagButton(sender: UIButton) {
    if naggingNotification.nagging{
       naggingNotification.stopTimer()
    }else{
       naggingNotification.startTimer(10.0)
    }
      stateDidChange()
}

This code uses the naggingNotification read-only property nagging to determine if the timer is on. We toggle the timer depending on the result, using the naggingNotification methods stopTimer and startTimer. We’ve set up this demo with a 10 second timer.

Our last statement calls a method we have yet to define stateDidChange(). We’ll update the button title with this. Add this code to ViewController:

func stateDidChange(){
    if !naggingNotification.nagging{
       nagButton.setTitle"Nag", forState: .Normal)
    }else{
       nagButton.setTitle("Stop Nagging", forState: .Normal)
    }
}

We have our code. Go to the storyboard. Drag one button to the center of the storyboard. Title it Nag, and set the font to 46 point Bradley Hand.

2016-05-01_07-49-18

In the lower right corner of the storyboard, you will find the auto layout controls. With the Nag button selected, click the align alignment icon icon to get the align menu. Towards the bottom of this menu you will find the center Horizontally in Container and Vertically in container settings. Click both on with values of zero. Set the Update frames to Items of New constraints.

2016-04-26_15-32-25

Open the assistant editor. Drag from both circles in the code to the button to connect the action and the outlet to the button.  Close the assistant editor.

Before we test this, launch both the iPhone and watch simulators. If you have never launched these before running an app, check the directions here Especially with the watch, it helps to have them running before you run an app on them.

Build and run using a iPhone 6s plus simulator. You get the notification message the first time you run this:

2016-04-29_10-09-50

Press OK to allow notifications.You get a simple screen of a single button:

2016-05-01_07-54-37

Tap the Nag button.

2016-05-01_07-54-44

The button toggles but doesn’t do more than that. we need to be outside the app to see a notification.

Press Command-Shift-H or on the hardware simulator menu, Hardware>Home. Wait a few seconds and you will see this:

2016-04-29_10-09-53

Swipe down from the status bar once the banner disappears. Select Notifications and you will see the notifications appear.

2016-04-29_10-09-54

Tap one of the notifications and you are back in the app. Tap Stop Nagging and the notifications turn off.

Adding Categories and Actions to your Notifications

Introduced in iOS8, Notifications  now have their own actions, which appear as buttons on a notification. Often you’ll find these in the AppDelegate.  I’ll however keep everything on one class for the notification. Actions are of class UIUserNotificationAction and it’s subclass UIMutableUserNotificationAction. Since you can’t change anything in a UIUserNotificationAction, we define actions in UIMutableUserNotificationActions. We’ll define two actions, one to stop the timer and one to change the nag message to emoji. In the NaggyNotification class add the following method:

func makeActions()-> [UIMutableUserNotificationAction]{
    let stopAction = UIMutableUserNotificationAction()
    stopAction.title = "Stop"
    stopAction.identifier = "StopAction"
    stopAction.destructive = true
    stopAction.authenticationRequired = true
    stopAction.activationMode = .Foreground
     
    let moreAction = UIMutableUserNotificationAction()
    moreAction.title = "Emoji"
    moreAction.identifier = "EmojiAction"
    moreAction.destructive = false
    moreAction.authenticationRequired = false
    moreAction.activationMode = .Background
        
    return [stopAction,moreAction]
    }

The title parameter is the title that will appears on buttons of the notification. The identifier is the string used to internally identify the action. You’ll notice that there is no code here to execute the action. We’ll use that identifier to code the action in AppDelegate. We can add a red warning to a button if it is a destructive button, one that deletes something. I used it here as the Stop button, but left destructive as false for the Emoji button. The activationMode has two choices: .Foreground or .Background and indicates if the app will execute the action in the foreground or the background. To maintain security of the user’s phone we can also require an authentication before we execute the action. For actions in the foreground, this is mandatory and activationMode is always true. The statement in our code is redundant.

We return an array of the actions. We will use these actions in a category. Categories contain all information about a specific type of notification. Our category, which we’ll call Nags contains the actions that will show in the nag notification. Add the following code to the NaggyNotification class.

 //MARK: - Categories and Actions
    func makeCategory()-> UIMutableUserNotificationCategory {
        let category = UIMutableUserNotificationCategory()
        category.identifier = "Nags"
        category.setActions(makeActions(), forContext: .Default)
        return category
    }

We make set two properties on a UIMutableUserNotificationCategory. We set an identifier Nags and using the setActions:forContext: method add the actions to the category.

We’ve set up our category and actions. Now to use them. We’ll do that in AppDelegate. Find the method we changed earlier. Change the highlighted lines to add the category to our current code.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) ->; Bool {
        // Override point for customization after application launch.
        let category = naggingNotification.makeCategory()
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(
                forTypes:[.Alert, .Sound, .Badge],
                categories: [category]
            )
        )
        return true
    }

If we ran our code now, we would have our actions listed in the notification, but nothing would happen. We need to add a callback method to AppDelegate containing the code to execute the actions. Add this to AppDelegate

    func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
        
        if notification.category == "Nags"{
            if identifier == "StopAction"{
                naggingNotification.stopTimer()
            }
            if identifier == "EmojiAction"{
                naggingNotification.toggleAlertMessages()
            }
        }
        completionHandler()
    }
  

The application:handleActionWithIdentifier:forlocalNotification method has a actionIdentifier and a UILocalNotification. for parameters.   The code checks the notification’s title. If it is our notification, we check the action identifier and perform the action we need.

We need one more method back in NaggyNotification to toggle the alert messages. Add this to the code.

func toggleAlertMessages(){
        if nags == basicNags {
            nags = emojiNags
        } else {
            nags = basicNags
        }
    }    

Fixing a Bug Before It Happens

This is all the code we need to run our application. However, there’s one bug that we’ll fix first. The problem is the Stop action when we are in the background. It will stop the the timer and shut down the nags. The button on our view controller does not know this. With the code as it is written the button still says Stop Nagging when we enter the foreground. We need to update the button any time we do enter the foreground. Find the applicationDidBecomeActive in AppDelegate. This method runs when we need to do that refresh of our user interface. Change it to this

    func applicationDidBecomeActive(application: UIApplication) {
        let rvc = window?.rootViewController as! ViewController
        rvc.stateDidChange() //Update the button
    }

Line 2 of the code gets the view controller. We have the view controller as our root view controller, so it’s easy to access. Once we have a reference to the root controller, we can call its method. statDidChange which checks the status of the timer and changes the title to the correct one.

Build and run.

2016-05-01_07-54-37

Press the nag button. Press Command-Shift-H. Eventually the notification appears:

2016-05-01_07-09-56

Drag the small oval at the bottom of the notification down.

2016-05-01_07-37-48

The actions appear.

2016-05-01_07-11-10

Tap the emoji button.  The next notification you get is an emoji.

2016-05-01_07-11-41

Drag down from the status bar and get the notifications. Swipe right to left on one of the notifications.

2016-05-01_07-15-52

Tap the Stop button. The app stops and opens up to the Nag screen again.

2016-05-01_07-54-37

The Apple Watch for All Developers

While that’s what you need to know for iOS, it’s not the whole story. Unless notifications are turned off by the user of an Apple watch, the watch will receive the notification if the phone is asleep.  Sadly, the watch simulator does not always reflect what happens on the watch.  You can try using the simulator if you  don’t have a watch handy, I’ll be using both since they have slightly different behaviors.

If your app is still running, get the watch and phone  simulator visible

2016-05-01_16-39-08

Start nagging with the phone, then press Command-L to Lock the phone. The simulator goes black. Wait a few seconds,  And you will see the Short-Look Notification appear:

Photo May 01, 9 26 23 AM

A second later, the  Long-Look notification appears.

2016-05-01_16-39-07

You’ll notice the buttons for the actions don’t quite fit on the watch. scroll up and you’ll find one more button.

2016-05-01_16-45-22

Test the emoji button.  Tap it, and the next notification changes to this:

2016-05-01_16-51-29

Press the stop button and you get a blank screen with the time. Actually, you are running a blank watch app.

2016-05-01_16-53-02

 You get slightly different  results with a real phone. Running the app on my phone, I start nagging by pressing the Nag button. I then lock my phone  by pressing the switch. SInce I turned on sounds and used the system sound in my actions, I’ll get a Haptic vibration on my wrist,  and then the notification.

Photo May 01, 9 25 40 AM

The notification includes the Emoji button and a dismiss button, which is cut off.  You can scroll up to get the rest of the dismiss button. On the simulator, we had the stop button, but on the watch it’s missing.

The difference has to do with activationMode. If you have an action with an activationMode of .Foreground the watch does not display it automatically. The foreground mode on the watch starts the watch app associated with this notification. In the simulator it lets us run that blank app, but on a physical watch it knows there is no app and doesn’t show the button.  On the other hand, an activationMode of .Background on the watch sends to the background of the phone. That’s why with no coding, the Emoji button works perfectly. Go to the NaggyNotification class. In makeActions, change our code to put the Stop button in the background

stopAction.activationMode = .Background

Now run the app again and when we lock the phone we get both buttons on the watch:

Photo May 01, 10 05 16 AM

With more buttons, the dismiss button is missing. Drag up and you will find it:

Photo May 01, 10 10 27 AM

Tap the Emoji Button. The next notification changes to emoji

Photo May 01, 10 05 31 AM

Tap the Stop button. On both the live watch and the simulator, the timer stops.  All of our actions work on the watch without a single new line of code.

Notifications will happen on an Apple Watch even if you don’t program for it. For Apple watch developer, this provides one way to launch your watch app. For iOS developers using notifications and actions, it means you have one more thing to think about when writing an app.  Think out what you want to show and not to show  for a watch user. If you are not writing a watch app, you can exclude actions from the watch but not the phone by making the  activationMode go to the foreground.

The whole code

NaggyNotification.swift

//
//  NaggyNotification.swift
//  PhoneWatchNotificationDemo
//
//  Created by Steven Lipton on 5/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

//Global variable -- not always a good idea,
// but makes running this code much simpler
//
let naggingNotification = NaggyNotification()

class NaggyNotification: NSObject {
    
    //All properties are private or read only 
    //Good insurance for using the global instance
    //nothing gets messed up. 
    
    private let basicNags = ["Nag", "Nag nag","Naggity nag"]
    private let emojiNags = ["😀","😉","🙄","😒","😠","😡"]
    private var nags = ["Nag", "Nag nag","Naggity nag"]
    private var counter = 0
    private var timer = NSTimer()
    private var backgroundTask = 0
    private var localNotification = UILocalNotification()
    var nagging:Bool {return timer.valid } //read only
    
    func presentNotificationNow(){
        UIApplication.sharedApplication().presentLocalNotificationNow(localNotification)
    }
    func toggleAlertMessages(){
        if nags == basicNags {
            nags = emojiNags
        } else {
            nags = basicNags
        }
    }
    
    func startTimer(timeInterval:NSTimeInterval){
        if !timer.valid{ //prevent more than one timer on the thread
            counter = 0
            backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
            timer = NSTimer.scheduledTimerWithTimeInterval(
                timeInterval,
                target: self,
                selector: #selector(timerDidFire),
                userInfo: nil,
                repeats: true)
        }
        
    }
    
    func timerDidFire(timer:NSTimer){
        counter += 1
        let currentNag = counter % nags.count //cycle through messages
        let nagString = String(format:"# %i %@",counter,nags[currentNag])
        localNotification.alertBody = nagString
        localNotification.alertTitle = "Naggy Notification"
        localNotification.category = "Nags"
        //localNotification.alertTitle = localNotification.category
        localNotification.soundName = UILocalNotificationDefaultSoundName
        localNotification.userInfo = ["NagNum":counter,"NagString":nags[currentNag]]
        presentNotificationNow()
        
    }
    
    func stopTimer(){
        timer.invalidate()
        UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
    }
    
    //MARK: - Actions and Categories
    func makeActions()-> [UIMutableUserNotificationAction]{
        let stopAction = UIMutableUserNotificationAction()
        stopAction.title = "Stop"
        stopAction.identifier = "StopAction"
        stopAction.destructive = true
        stopAction.authenticationRequired = true
        stopAction.activationMode = .Background
        
        let moreAction = UIMutableUserNotificationAction()
        moreAction.title = "Emoji"
        moreAction.identifier = "EmojiAction"
        moreAction.destructive = false
        moreAction.authenticationRequired = false
        moreAction.activationMode = .Background
        
        return [stopAction,moreAction]
    }
    
    func makeCategory()-> UIMutableUserNotificationCategory {
        let category = UIMutableUserNotificationCategory()
        category.identifier = "Nags"
        category.setActions(makeActions(), forContext: .Default)
        return category
    }
}

AppDelegate.swift

//
//  AppDelegate.swift
//  PhoneWatchNotificationDemo
//
//  Created by Steven Lipton on 5/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        let category = naggingNotification.makeCategory()
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(
                forTypes:[.Alert, .Sound],
                categories: [category]
            )
        )
        return true
    }
    
    func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
        
        if notification.category == "Nags"{
            if identifier == "StopAction"{
                naggingNotification.stopTimer()
            }
            if identifier == "EmojiAction"{
                naggingNotification.toggleAlertMessages()
            }
        }
        completionHandler()
    }
    

      func applicationDidBecomeActive(application: UIApplication) {
        let rvc = window?.rootViewController as! ViewController
        rvc.stateDidChange() //Update the button
    }

   
}



ViewController.swift

//
//  ViewController.swift
//  PhoneWatchNotificationDemo
//
//  Created by Steven Lipton on 5/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit


class ViewController: UIViewController {
    @IBOutlet weak var nagButton: UIButton!
    @IBAction func nagButton(sender: UIButton) {
        if naggingNotification.nagging{
            naggingNotification.stopTimer()
        }else{
            naggingNotification.startTimer(10.0)
        }
        stateDidChange()
    }
    func stateDidChange(){
        if !naggingNotification.nagging{
            nagButton.setTitle("Nag", forState: .Normal)
        }else{
            nagButton.setTitle("Stop Nagging", forState: .Normal)
        }
    }
}

The Complete Table Tutorial for WatchOS2

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

Make a New Project

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

Add Your Controls

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

2016-03-17_07-28-43

In the object catalog, find the table object.

2015-07-30_05-26-25

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

2016-03-17_07-31-48

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

2016-03-24_07-17-17

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

2016-03-24_06-57-45

Click on the Table Row Controller in the document outline. Row controllers need unique names in the table. In the attribute inspector, make the identifier row

2015-07-30_05-39-09

Click the group for the row controller. By default, this group is horizontal. For our app, set the Layout to Vertical. Set the Height Size to Size to Fit Content. Drag two labels into the group. Title them like this:

2016-03-24_07-16-35

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

2016-03-24_07-14-52

Adding and Connecting View Controller Classes

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

Connecting the Info controller

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

2016-03-24_07-25-52

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

 @IBOutlet var paceLabel: WKInterfaceLabel!

Connecting the Table Row Controller

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

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

Go to the storyboard. Click on the row controller named row in the document outline. In the identity inspector, change the Class to TableRowController. Open the assistant editor. Xcode assumes you want to work with the interface. Most likely you will see the InterfaceController.swift file in the assistant editor. Click at the top where it says Automatic and select Manual. Select through the choices to get the Watchkit Extension file TableRowController.swift. From the circles to the left of the outlets, connect the outlets to the controls on the storyboard by dragging from the circle next to the code to the control. We’ve now connected the row controller to the storyboard.

Setting the View Controller

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

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

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

Adding a Model Class

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

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

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

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

Make the Table

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

 let runData = RunData()

Add the following method to the class

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

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

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

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

Photo Mar 29, 6 24 06 AM

Selecting Rows in a Table

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

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

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

class InfoInterfaceController: WKInterfaceController {

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

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

Build and run.

Click on the split for 5 miles,

Photo Mar 29, 6 24 06 AM

and you get an average pace for the split.

Photo Mar 29, 6 24 09 AM

Click Info to return to the table.

Adding a Context Menu

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

2016-03-29_06-33-56

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

2016-03-29_06-34-40

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

2016-03-29_06-43-20

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

2016-03-29_06-35-30

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

2016-03-29_06-35-52

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

2015-08-11_05-57-03

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

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

Close the assistant editor for now.

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

2016-03-29_06-50-24

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

Photo Mar 29, 6 55 00 AM

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

Adding Reset

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

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

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

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

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

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

Selecting the Row

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

var selectedRow:Int! = nil

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

 selectedRow = rowIndex //for use with insert and delete

Adding An Alert

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

2015-08-11_06-59-51

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

2016-03-29_13-37-55

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

Deleting Rows From a Table

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

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

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

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

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

Photo Mar 31, 6 27 47 AM

Go back to the table and select the 9 mile.

Photo Mar 31, 6 28 25 AM

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

Photo Mar 31, 7 35 53 AM

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

Adding a Row to the Table

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

Adding Items  in the Model

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

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

Make a Input Controller

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

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

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

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

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

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

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

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

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

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

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

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

Photo Mar 31, 7 38 19 AM

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

Photo Mar 31, 7 41 21 AM

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

Photo Mar 31, 6 28 30 AM (1)

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

Photo Mar 31, 6 28 55 AM (1)

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

Photo Mar 31, 7 45 36 AM (1)

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

Photo Mar 31, 7 50 45 AM

Say ” Time eight three five seconds”

Photo Mar 31, 7 50 54 AM

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

Photo Mar 31, 7 51 04 AM

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

Photo Mar 31, 7 51 46 AM

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

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

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

Comment out the line

//var seconds = 0 

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

Subtotals and Multiple-Row Tables

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

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

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

table.setRowTypes(rowTypes)

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

2016-03-31_08-01-22

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

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

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

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

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

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

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

Some people will only use a header and a footer.

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

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

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

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

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

Change the Storyboard

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

2016-03-31_08-05-37

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

2016-03-31_08-08-53

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

2016-04-01_06-05-10

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

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

2016-03-31_08-20-47

Add TableRowControllers

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

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

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

Close up the assistant editor.

Coding the Multiple Row View Controller

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

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

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

The refreshTableData() method

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

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

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

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

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

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

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

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

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

Modifying the tableRefresh Method

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

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

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

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

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

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

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

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

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

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

 

Build and run.  The splits show up.

Photo Apr 01, 9 38 49 AM

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

Selection with Multiple Rows

Why did it fail?  Look at the  didSelectRowAtIndex code.


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

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

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

Change

selectionIndex = rowIndex

 to this:

selectionIndex = tableSplit[rowIndex]

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

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

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

to this:


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

Build and run. You can now select a row.

Photo Mar 29, 6 24 09 AM

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

The Whole Code

Interfacecontroller.swift

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

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {

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

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

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

}

RunData.swift

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

import WatchKit

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

InfoInterfaceController.swift

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

import WatchKit
import Foundation


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

}

TableRowController.swift

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

import WatchKit

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

Split5KTableRowController.swift

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

import WatchKit

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

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

Using WatchOS2 Navigation in Swift

It’s rare to have a one controller application, even in something as small as the Apple watch. Multiple View Controllers, or Interface Controllers as they are called in WatchKit, need ways to move between controllers. WatchOS2 has a simplified version of the iOS navigation types. In this lesson we’ll explore these three types of navigation that are available for WatchOS: Page, Hierarchical, and Modal. You’ll find them summarized on this chart below

2016-03-04_12-03-25

In this tutorial we’ll show all three programmatically and on the storyboard.

Set Up the Project

Before starting the project, start the watch and phone simulators. If you do not have direct access to them, you might want to read the Before you Begin section in a earlier post.

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

2016-02-03_05-34-40

You’ll get a configuration window like this:

2016-03-02_06-25-53

Name the project WatchNavigationDemo using Swift as the language. Use a Universal device and uncheck Include Notification Scene as shown above. In the Navigator find these files:

2016-03-02_06-29-30

The Root Controller

Click on the Interface.storyboard in the WatchNavigationDemo WatchKit App group. You will get a blank watch scene with a watch controller and a notifications controller. Drag two buttons and a label to the controller. Set the background color to the system Dark Gray Color(#555555) for both buttons. In the attributes inspector, set one button’s alignment to Top Vertical Alignment, and the other button’s alignment to Bottom Vertical Alignment. Set the label’s text to Root and the font to Headline. Set the label’s vertical and horizontal alignment to Center.

2016-03-02_06-37-05

Click the view controller icon. At the top of the attribute inspector, add a Title and Identity of Root

2016-03-02_06-42-12

For each of our controllers we will add and identity and title. Title will set the title on the watch face. The Identifier names this controller programmatically. If you are only using segues, then you do not need it, but it is mandatory if you are programmatically calling your controllers. If you have neither a segue nor identifier you will get a warning error from Xcode that the controller is unreachable. With our controller done, it should look like this:

2016-03-01_06-11-01

The Page Controllers

Drag another interface controller to the storyboard. Change the background to Green(#AACC00). Add a button and a label. Set the vertical alignment for the button to Bottom. Set the background color to the system Dark Gray Color(#555555). Select the controller and change the Title and Identifier to Page 0. Set the label’s text to Page 0. Set the label’s vertical and horizontal alignment to Center.

Your controller should look like this:

2016-03-01_06-12-42

Select the interface controller icon, then press Command-C to copy the controller. Deselect the controller by clicking on the background of the storyboard. Press Command-V to paste a copy of the controller. This pastes the controller directly on top of the original. It looks like the original controller is selected again. Drag this controller and you will find it is a copy.

2016-03-02_07-09-40

Deselect, and press Command-V again. You now have three Page 0 controllers. Change the Identifier and Title of one copy to Page 1 and the other to Page 2. Arrange the controllers like this

2016-03-02_07-16-45

The Modal Controllers

Add two more controllers, which we’ll use as our modal controllers. Make one controller have a Red(#FF0000) background and the other a Blue(#0000FF) background. Set the Title and Identifier of the red modal to RedModal. Set the Title and Identifier of the blue modal to BlueModal.

2016-03-01_06-10-36

Arrange your controllers like this:

2016-03-02_07-32-44

Page Navigation Through Segues

Page navigation is similar to iOS page navigation  with pages sliding horizontally with a swipe. It is much simpler to set up than the iOS equivalent. All the glances on an apple watch are in a page sequence. If you don’t have a watch you can still see a quick example in the watch simulator. You can swipe up from the watch face to get to glances. Swipe left and right to see two glances.

Like most storyboard transitions between controllers we use segues. For page segues, We control-drag from one controller to another controller, then repeat to make the chain of controllers. Control-drag from the black background of Root to the green of Page 0. When you release the button, a Relationship Segue menu appears:

2016-03-01_06-18-24

Select Next Page. A segue appears.

2016-03-02_08-02-00

Control-drag from Page 0 to Page 1, repeating the process. Then do it again from Page 1 to Page 2. We have four pages connected together.

2016-03-02_08-06-51

Build and run. You have four pages to scroll back and forth through.

page navigation

Hierarchy Navigation by Segue

Like navigation controllers in iOS, hierarchy navigation pushes the next controller to view through a selection device, such as a button. As the name implies you can branch in as any direction you want, unlike the linear page navigation. The settings app on the Apple Watch is an example of a hierarchy navigation you can see on both the watch and the simulator.

Delete all the segues on the storyboard. Select the top button on the root controller. Control-drag from the top button to the Page 0 controller. When you release the mouse button, you will get this menu:

2016-03-01_06-13-40

Select Push. A segue appears between the controllers.

2016-03-03_05-31-22

The process is almost the same as the page segues. The difference is you start your drag from a control, usually a button but it may be a group or table. Make a segue between the bottom button of Root and Page 1. Control drag from the bottom button of Root to Page 1. Select Push for the Action Segue type.

Page and Hierarchy are Mutually Exclusive

Control-drag from Page 1 to Page 2 and make a page segue. Your storyboard should look like this.

2016-03-03_05-46-53

Xcode does not put any warnings up, but what we just did is wrong. You cannot combine page and hierarchy segues. You can only use one or the other on the storyboard. With one exception, once you use one type or segue or the other. You are stuck in the project using that type of navigation only. However, nothing tells you this except the documentation. Clean the project with a Shift-Command-K, then Build and Run. Tap the top button, and you get Page 0.

At the top left of the watch is a back indicator which you tap to go back to root. If you try going to page 1 with the bottom button, there is no page indicator on the Page 1 to get us to page 2. Swipes don’t work either. The system will ignore any illegal segues.

Stop the app and delete the page segue. Control-drag from the button on Page 1 to Page 2 and select the Push segue. Clean, Build and Run. You get all three pages.

hierarchy navigation

Adding Modal Controllers

The last type of controller does not care what type is the one before it. Modal controllers can be called by both page and hierarchy controllers. While the two we’ve learned so far transition horizontally, modal controllers transition vertically. In the watch or simulator, swipe up on the watch face and you have a modal to the glances. The glances, as we discussed earlier, happen to be page navigation. Here’s an exception to the mutual exclusion between hierarchy and pages. If you have a hierarchy then segue to a modal, your modal can segue to pages. However it does not work the other way around. From a page, you cannot call a modal and then a hierarchy.

You set a Modal segue the same way as a hierarchical segue: from control to controller. Stop the app. From the Page 0 button, control-drag to the RedModal controller. Select Modal in the Action Segue menu

2016-03-01_06-23-17

You get a segue between the two.

2016-03-03_06-43-33

Control-drag from the Page 2 button to the BlueModal controller. Select a Modal action segue. From the BlueModal, control-drag to the RedModal. Select the Page relationship segue. Your storyboard should look like this:

2016-03-03_06-44-38

Build and run. All the navigation works.

modal navigation

Programmatic Navigation

We’ve covered navigation on the storyboard. While the mutual exclusive part does provides some limits for building apps, there is enough flexibility to set up your entire app in the storyboard. It’s probably best to setup your applications in the story board, as many of the attributes of a controller are not accessible programmatically. However, many developers prefer programmatic navigation to segues. Sometimes the logic of the code dictates a more programmatic approach. There are three methods that call controllers directly, as shown in the chart above. We’ll discuss each as we code the controllers.

Press Command-N to make a new file. Make a new WatchOS Watchkit file named PageIntefaceController, subclassing WKInterfaceController. Save the file being careful to make sure the group is in the extension.

In the file that appears, add the following code.

    var whichModal = "RedModal"
    @IBOutlet var goModalOutlet: WKInterfaceButton!
    
    @IBAction func goModalAction() {
        presentControllerWithName(whichModal, context: nil)
    }

In the action goModalAction, the method presentViewControllerWithName:context: presents the modal controller with the identifier name from the storyboard. We’ve used a string variable whichModal, which we’ll use in a later iteration to switch between the blue and red modals. The second parameter context, sends data to the destination controller. We set this to nil to not use it. We’ll discuss this parameter in mmore detail below.

Select the InterfaceController.swift file. Add the follow code to the InterfaceController class

@IBAction func topButton() {
    pushControllerWithName("Page 0", context: nil)
}
@IBAction func bottomButton() {
    let names = ["Page 2","Page 1","Page 0"]
    presentControllerWithNames(names, contexts: nil)
 }

The bottomButton action, contains the presentControllerWithNames method. This presents the page views. It has similar parameters to presentControllerWithName, with one change: instead of a single value it uses arrays. The array order is the order of the pages displayed. Our array is in the constant names, counting down pages instead of up as we have been doing. The contexts array has the corresponding context to in the names array. We are not using it yet, so we left that nil.

The action topButton has the method for the hierarchical segue pushControllerWithName:context: Like a navigation controller, it pushes the new interface into view. The parameters are identical to its modal brother.

Connect up your outlets and actions. Go back to the story board, and select the Page 0 controller. In the identity inspector, change the class to PageInterfaceController. Repeat for Page 1 and Page 2. Close the right panel inspector and open the assistant editor. Click on the Root Controller in the storyboard. Drag from the topButton action to the upper button. Drag from the bottomButton action to the bottom button.

Select the Page 0 controller. Connect the outlet and action to the button. Do the same for Page 1 and Page 2.

Clean, build and run. From the root menu, click the top button.

2016-03-03_06-05-00

It goes to a hierarchical controller version of Page 0. Tap the button in Page 0 and we get the red modal.

Go back to the root, and tap the bottom button. You get a page controller starting with Page 2 and counts down pages to 0.

2016-03-03_06-05-30

Tap any button on a controller goes to the red modal.

programmatic navigation

We seem to have broken the mutual exclusive nature of the page and hierarchy controllers. Programmatically,  there’s one more wrinkle. The system does not know which navigation you use until your first use of it on the root. In our app, we set that by the button pressed.

Working with Contexts

Contexts pass data to a destination controller. In the modal and Hierarchical controllers it is a single AnyObject? For pages it is an optional array of AnyObject. Close the assistant editor, and go to the InterfaceController.swift file. Change the actions to this:

    @IBAction func topButton() {
      pushControllerWithName("Page 0", context: "BlueModal")
    }
    @IBAction func bottomButton() {
        let names = ["Page 2","Page 1","Page 0"]
        let contexts = ["RedModal","BlueModal","RedModal"]
        presentControllerWithNames(names, contexts: contexts)
    }

In topButton we pass a string as a context. In bottomButton we added a string array contexts that will pass the string RedModal for Page 0 and 2 and a BlueModal for page 1.

Go to the PageInterfaceController code. Change the awakeWithContext method to this:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    if let newContext = context{
        whichModal = newContext as! String
        goModalOutlet.setTitle(whichModal)
    }
}

The awakeWithContext method is the equivalent to viewDidLoad in iOS, except it receives a context. We can use that context to set up the class. First optionally chain the context’s value. Then assign it, downcasting appropriately. I’m setting which modal we will use this way, and also setting the button’s title to tell you where you are going.

Build and run. Select the bottom button and you will see the contexts changing the page controllers.

contexts

What if we wanted to make a more elegant title for the button? A common way of passing more than one parameter in the context is to use a dictionary. Change InterfaceController’s actions to this:

@IBAction func topButton() {
    let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
    pushControllerWithName("Page 0", context: context)
}
@IBAction func bottomButton() {
    let names = ["Page 2","Page 1","Page 0"]
    let contexts:[[String:AnyObject]] = [
        ["Name":"RedModal","Title":"Red"],
        ["Name":"BlueModal","Title":"Blue"],
        ["Name":"RedModal","Title":"Red"]
    ]
    presentControllerWithNames(names, contexts: contexts)
}

Using a dictionary of type [String:AnyObject], I pass two strings to the array. I could use a [String:String], but I want to demonstrate the more generic case where you could use more than one type of data in the dictionary. Go to PageInterfaceController. Change awakeWithContext like this:

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        if let newContext = context{
            let contextDictionary = newContext as! [String:AnyObject]
            whichModal = contextDictionary["Name"] as! String
            let title = contextDictionary["Title"] as! String
            goModalOutlet.setTitle(title)
        }
    }

We changed the code to read a dictionary instead of a single value. You could do the same with a class or struct as well, though dictionaries are nicely temporary in comparison. Build and run. Click the top button and you find the Blue titled button

2016-03-04_07-07-28

Go Back to Root, then click the bottom button, we have a Red button

2016-03-04_07-07-27

Contexts with Segues

Watchkit also has a equivalent to prepareForSegue for passing data when you use a storyboard segue. Like prepareForSegue, you override the method. For modals and hierarchy there is the method For pages there is the to accommodate the array of contexts. Go to the storyboard and add a push segue between Root and Page 0

2016-03-03_05-31-22

In the attributes inspector, set the Identifier for the segue to Page0. Go to InterfaceController. Comment out the code in topButton

@IBAction func topButton() {
    //let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
    //pushControllerWithName("Page 0", context: context)       
}

Add the following function to your code

override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
    
if segueIdentifier == "Page0"{
    let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
        return context
    } else {
        return nil
    }        
}

The contextForSegueWithIdentifier works similar to prepareForSegue. Take  segueIdentifier and do what you need to set up the destination controller for that specific segue. The difference is we return the context, instead of setting a properties of the destination controller directly. For pages, we do the same thing using arrays and contextsForSegueWithIdentifier

Clean, build and run. Tap the top button on Root You should see no difference between the last run and this one.

2016-03-04_07-07-28

That is most of what you need to know about navigation. There are two or three more thing you might want to know such as dismissals and delegates. We’ll cover those next time as we delve into using tables in WatchOS2.

The Whole Code

InterfaceController.swift

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

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {
    
    @IBAction func topButton() {
        // code commented out for using segues and context
        // code for hierachy (push) segues
        //let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
        //pushControllerWithName("Page 0", context: context)
        
    }
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        // the prepareForsegue of WatchOS for modals and push
        // use contextsForSegueWithIdentifier for pages
        if segueIdentifier == "Page0"{
        let context:[String:AnyObject] = ["Name":"BlueModal","Title":"Blue"]
            return context
        } else {
            return nil
        }
        
    }
    @IBAction func bottomButton() {
        // A Page controller sequence with a dictionary as a context.
        let names = ["Page 2","Page 1","Page 0"]
        let contexts:[[String:AnyObject]] = [
            ["Name":"RedModal","Title":"Red"],
            ["Name":"BlueModal","Title":"Blue"],
            ["Name":"RedModal","Title":"Red"]
        ]
        presentControllerWithNames(names, contexts: contexts)
    }
   
}

PageInterfaceController.swift

//
//  PageInterfaceController.swift
//  WatchNavigationDemo
//
//  Created by Steven Lipton on 3/3/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class PageInterfaceController: WKInterfaceController {
    var whichModal = "RedModal"
    @IBOutlet var goModalOutlet: WKInterfaceButton!
    
    @IBAction func goModalAction() {
        //present a modal controller
        presentControllerWithName(whichModal, context: nil)
    }
    override func awakeWithContext(context: AnyObject?) {
        // the viewDidLoad of WatchKit/WatchOS. 
        // has a parameter which has the context sent to the controller 
        // So you can set the controller up 
        // we use a dictionary  of [String:AnyObject]
        super.awakeWithContext(context)
        if let newContext = context{
            let contextDictionary = newContext as! [String:AnyObject]
            whichModal = contextDictionary["Name"] as! String
            let title = contextDictionary["Title"] as! String
            goModalOutlet.setTitle(title)
        }
    }
}