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:
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.
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:
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.
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.
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:
Tap the switch to change it. We get a different workout:
Tap the Work Out!! button.
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:
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.
Take the timer to a whole minute and you find something interesting: it just stops and does nothing.
There 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() } }
Leave a Reply