Make App Pie

Training for Developers and Artists

Swift WatchKit Tutorials: Programming Buttons, Switches and Timers for Apple Watch

Screenshot 2015-04-15 08.34.58
Once you start connecting outlets and actions for an Apple Watch you realize this is not your friendly neighborhood iOS app. There are controls that look familiar in storyboard, but are far from the same in code.  In this lesson, you’ll learn about the familiar button and switch from iOS in a WatchKit setting, where things get strange rather quickly. We’ll also add functionality to the timer introduced in the last lesson. Most of all, You’ll learn why WKInterface objects are not what they seem.

I’ve been gaining weight while writing my book Swift Swift View Controllers. I need to get in some runs for exercise. I’m a big fan of run/walk intervals when I’m running. I’d like to track that on my watch. In the app we are creating, I’d like to have a countdown of my walk and run intervals.  I’d also like to stop and grab a slice of pizza every so often.

Connect the Outlets and Actions

In our last lesson,we did the layout for that app.  If you do not already have it open, open the SwiftWatchRunWalk app in Xcode.   Select the WatchKit app scene. Open the Assistant editor. Set it to Automatic to make sure you have the InterfaceController.swift file selected.

Control drag from the timer to the code. You get a popup like this, very similar to the iOS version:

Screenshot 2015-04-16 05.37.07

Set the name of the timer as runTimer.  You get an outlet in your code.

@IBOutlet weak var runTimer: WKInterfaceTimer!

Do the same for the untitled label to the right of the Eat Pizza!! label.  Name it statusIconLabel.  Make an outlet for the Eat Pizza!! label named statusTextLabel. Make an outlet for the label next to the button named workoutIconLabel. You should now have these outlets:


//MARK: - Outlets
    
    @IBOutlet weak var runTimer: WKInterfaceTimer!
    @IBOutlet weak var statusIconLabel: WKInterfaceLabel!
    @IBOutlet weak var statusTextLabel: WKInterfaceLabel!
    @IBOutlet weak var workoutIconLabel: WKInterfaceLabel!

Control-drag from the switch to the code. Change the Connection to Action. The action popup appears, but looks very small compared to the  iOS version.

Screenshot 2015-04-16 05.53.52

Add the Actions and a Problem

We have only one action for objects in WatchKit.  We also have no sender. Name the action runWalkSwitch and click Connect. We get very different code for our action than iOS:

@IBAction func runWalkSwitch(value: Bool) {

}

The parameter is value:Bool not sender:AnyObject. Watchkit, unlike UIKit, has no properties on the controls. Everything you control programmatically will be a method. Actions have for their parameters what we would assume to be a property of a control. In iOS, we would use the on property of a UISwitch. For a WKInterfaceSwitch action, that is a  Bool parameter named value indicating if the switch is on or off.

Set up the button. Control-drag from the button to the code. Create an action named workoutButton. You get code like this:

@IBAction func workoutButton() {
}

For a button, there are no parameters. It triggers when you tap it. This presents a problem. We want to change the titles of the button and the switch. Usually we would do that with sender. Instead we have to make outlets. Control drag from the button to the code again.  Name an outlet workoutButton.  You will get an error on either your outlet or action:

InterfaceController.swift:49:20: Invalid redeclaration of 'workoutButton()'

We’ll get to this error after we connect the switch.  Control drag the switch to the code. Create an outlet named runWalkSwitch.  This give no error.  Why one and not the other?

The reason is the parameters. While I won’t get into details, as far as the Swift compiler is concerned, an outlet is a function. The outlet workoutButton is workoutButton(). We just don’t see the extra parentheses.  We have an action also named workoutButton(), so there’s a conflict. We don’t have that conflict with the switch which has a property of runWalkSwitch() and an action of runWalkSwitch(value:Bool). Because the parameters are different, they are different identifiers. In iOS, we never have this problem since we almost always have a sender parameter on actions.

To fix this, we will change the name of the action. Right click on the button. You will get the connections popup:

Screenshot 2015-04-16 08.54.43

Under Sent Actions, Click the X  after the Interface Controller to disconnect the action.

Change the action to this:

@IBAction func didPressWorkoutButton() {
}

We have a different, more descriptive identifier, and the error disappears. To make sure we get a good connection, clean the project by clicking Product>Clean from the menu. To make the connection, drag from the empty  circle just to the left of the method in the assistant editor back to the button. When the button highlights, release the button. We are connected again.

Adding Code to the Project

For our first iteration of code, change the didPressWorkoutButton to this:

@IBAction func didPressWorkoutButton() {
    isWorkingOut = !isWorkingOut //2
    if isWorkingOut{
    //set up for workout
        statusTextLabel.setText("Work Out!!")//5
        workoutButton.setTitle("Eat Pizza")
    } else {
    //show pizza
        statusTextLabel.setText("Eat Pizza!!")//9
        workoutButton.setTitle("Work Out!!")
    }
}

We’ve made a basic toggle button. Line 2  has a Bool variable isWorkingOut that we change every time we run the method. Using an if..else clause we switch back and forth between them. Lines 5 and 9  set the statusTextLabel‘s text using a method setText. Similarly lines 6 and 10 set the title of the button using the method setTitle. WKInterface controls do not have properties. You cannot read properties nor assign a value to them because they do not exist. There are attributes in the storyboard, but they are not accessible by code. You have methods to  do anything with.

Usually, I’d use the label’s text in a toggle like this and test to see if it was one string. Otherwise it is the second string. We have to set a variable here to get this to work, and I used a Bool to make it easy to switch between the two. We still need to declare it though. Add the following to your code above the outlets:

var isWorkingOut = false

Build and run. You can now change the label and the button title.

button working

Coding the Switch

Add the following code to the runWalkSwitch method:


@IBAction func runWalkSwitch(value: Bool) {
        //chages between walks and runs on the display
        isRunning = value
        if value{
            workoutIconLabel.setText(icon())
            runWalkSwitch.setTitle("Run")
        }else{
            workoutIconLabel.setText(icon())
            runWalkSwitch.setTitle("Walk")
        }
    }

We do the same as our previous method, though we have the parameter value we can use to determine the state of the switch. We do assign value to a variable isRunning for use in the rest of the class. We also use a method icon to set an emoji in those labels.  Add the icon method:


    //MARK: Instance Methods
    func icon() -> String {
        if isRunning{
            return runIcon  
        }else{
            return walkIcon
        }
    }

We used some constants to make this easier to use.  We’ll need to declare some things to get this to work. Add this to your code above the outlets:


 //MARK: Properties and constants
    let pizzaIcon = "🍕😍"
    let runIcon = "🏃🏻🏃🏿"
    let walkIcon = "🏻🚶🏽"
    var isWorkingOut = false
    var isRunning = true

If you have  Yosemite  10.10.3 or higher, you can use the multicolor emoji feature like I did to set my icons. Press Control-Command-Spacebar to get the symbols window. Navigate to Emoji and Activity. Click and hold on the icon and a menu pops up allowing you to pick a skin tone. I used two in each of my icons. Many programs  and web browsers do not meet this new emoji standard, so you may see a color square after the emoji in the listing above.

 emoji

If you do not have Yosemite 10.10.3,  use the regular icon.

Using the WatchKit  Life Cycle

in iOS we have viewDidLoad, and  viewWillAppear,  among other UIViewController methods for creating and ending a view controller. There are equivalent  WatchKit methods: awakeWithContext and willActivate .

We need to place the icons in the labels on the watch when the watch wakes up.  Change awakeWithContext to this:

//MARK: - Life Cycle
override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)

    // Configure interface objects here.
    statusIconLabel.setText(pizzaIcon)
    workoutIconLabel.setText(icon())
}

 Before we start the app, we configure the label icons to show and appropriate icon.  We put the pizza icons in the  status bar, and the runners in the workout icon.

We are ready to build and run:

Screenshot 2015-04-16 14.23.07

Tap the switch to change it. We get a different workout:

Screenshot 2015-04-16 14.23.22

Tap the Work Out!! button.

Screenshot 2015-04-16 14.34.14

We get a workout message but no workout icon. Change the method didPressWorkoutButton  to this:

 
@IBAction func didPressWorkoutButton() {
    isWorkingOut = !isWorkingOut
    if isWorkingOut{
    //set up for workout
        statusIconLabel.setText(icon()) //New code for icon
        statusTextLabel.setText("Work Out!!")
        workoutButton.setTitle("Eat Pizza")
    } else {
    //show pizza
        statusIconLabel.setText(pizzaIcon) //New code for icon
        statusTextLabel.setText("Eat Pizza!!")
        workoutButton.setTitle("Work Out!!")
   }
}

We change the statusIconLabel when we change modes.  Run again, and press the Work out!! button. Depending on the workout, we get the following:

Screenshot 2015-04-16 14.39.10 Screenshot 2015-04-16 14.39.18

Use the Timer

In our first app, we let the timer count up by checking the enabled setting in the storyboard. In this application, we’ll make a countdown timer for one minute. When we switch between walk and run the timer will reset and start counting from one minute again.

Add the following method:

func timerReset(){
        let interval:NSTimeInterval = 60.0
        runTimer.stop()
        let time  = NSDate(timeIntervalSinceNow: interval)
        runTimer.setDate(time)
        runTimer.start()
    }

This code stops the timer if already started. It then sets the time with a NSDate, which we use the timeIntervalSinceNow intializer to give us 60 seconds on the clock, then start the clock again.  We need to add some code to use this method.  Let’s start with didPressWorkoutButton. Add resetTimer() on the line under the code

workoutButton.setTitle("Eat Pizza")

add runtimer.stop() under the line

workoutButton.setTitle("Work Out!!")

In the runWalkSwitch method, add after the if..else clause, add this code.

//If in a workout, reset the timer
        if isWorkingOut{
            timerReset()
        }

We may change the switch while we are eating pizza and don’t want to run the timer then. So we check to make sure the timer can be on before resetting. Build and run our app. Start the workout and switch the timer a few times to watch the countdown.

run walk timer

Take the timer to a whole minute and you find something interesting: it just stops and does nothing.

end timerThere is no property or notification when the timer ends. The timer is merely cosmetic. For our app, it would be useful for us to have some action. In order to do that, we have to understand NSTimer,  which we will do in our next lesson.

The Whole Code

//
//  InterfaceController.swift
//  SwiftWatchButtons WatchKit Extension
//
//  Created by Steven Lipton on 4/14/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {
    //MARK: Properties and constants
    let pizzaIcon = "🍕😍"
    let runIcon = "🏃🏻🏃🏿"
    let walkIcon = "🚶🏻🚶🏽"
    var isWorkingOut = false
    var isRunning = true
    
    //MARK: - Outlets
    @IBOutlet weak var runTimer: WKInterfaceTimer!
    @IBOutlet weak var statusIconLabel: WKInterfaceLabel!
    @IBOutlet weak var statusTextLabel: WKInterfaceLabel!
    @IBOutlet weak var workoutIconLabel: WKInterfaceLabel!
    
    @IBOutlet weak var workoutButton: WKInterfaceButton! 
    @IBOutlet weak var runWalkSwitch: WKInterfaceSwitch!
    
    
    //MARK: - Actions 
    @IBAction func runWalkSwitch(value: Bool) {
        //changes between walks and runs on the display
        isRunning = value
        if value{
            workoutIconLabel.setText(icon())
            runWalkSwitch.setTitle("Run")
        }else{
            workoutIconLabel.setText(icon())
            runWalkSwitch.setTitle("Walk")
        }
        //If in a workout, reset the timer
        if isWorkingOut{
            timerReset()
        }
       
    }

    @IBAction func didPressWorkoutButton() {
        isWorkingOut = !isWorkingOut
        if isWorkingOut{
            //set up for workout
            statusIconLabel.setText(icon())
            statusTextLabel.setText("Work Out!!")
            workoutButton.setTitle("Eat Pizza")
            //start the timer
            timerReset()
        } else {
            //show pizza
            statusIconLabel.setText(pizzaIcon)
            statusTextLabel.setText("Eat Pizza!!")
            workoutButton.setTitle("Work Out!!")
            runTimer.stop()           
        }
        
    }
    
    //MARK: Instance Methods
    func icon() -> String {
        
        if isRunning{
            return runIcon            
        }else{            
            return walkIcon
        }        
    }

    func timerReset(){
        // a method to reset timer to 0 and start timer
        let interval:NSTimeInterval = 60.0
        //control the timer control on the interface
        runTimer.stop()
        let time  = NSDate(timeIntervalSinceNow: interval)
        runTimer.setDate(time)
        runTimer.start()
    }
 
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
        statusIconLabel.setText(pizzaIcon)
        workoutIconLabel.setText(icon())
    }

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

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

}

One response to “Swift WatchKit Tutorials: Programming Buttons, Switches and Timers for Apple Watch”

  1. […] We’ll continue from where we left off in the last lesson. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: