There is a horrible secret in WatchKit with many implications for those programming for the Apple watch. There is also a secret many people do not get about Apple’s corporate address. There two secrets are interestingly related. In WatchKit, There are no properties in the controls — you do everything with a method. This is especially annoying with the timer control you can add to the storyboard. It can count up or down, but can’t tell the system when it is at any given number. It is for cosmetic purposes only. In the documentation for timers, it tells you for any real timer functionality, you need to use NSTimer
.
For several lessons, we’ve built a watch app that allows the user to time their run/walk intervals on their watch. In this lesson, we’ll let the watch tell us when to change intervals.
We’ll continue from where we left off in the last lesson.
You can go there to find the code necessary to start this lesson.
Introducing NSTimer
Apple’s current corporate address is one Infinite Loop in Cupertino CA. Infinite loops are what made Apple their fortune. Starting with the Macintosh and continuing through the Apple watch, every Apple product runs an infinite loop at its heart. All of our programming, however linear it may seem, is just interruption of the infinite loop, or sub loops of the loop. We call the loop we run on a run loop. Generally, we do not care too much about it — it is far in the background. However, we can schedule an event on that loop at a certain time, which triggers a method to run. That scheduling is done by NSTimer.
While there are several ways to use NSTimer, usually we use the class method scheduledTimerWithTimeInterval
:
class func scheduledTimerWithTimeInterval( ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
The method schedules an event on the loop at several seconds using a NSTimeinterval.
When that happens, a method selector
on a object target
runs with parameters userInfo
. The timer can repeat or be a one shot, and we have a Bool
to tell us if we do or do not repeat. the method returns a NSTimer
object that we can use to control the timer.
In our simple interval for watch app, we will add a timer that signals a change between run and walk every minute. There are two strategies to do this. The easier of the two is set the timer for one minute, and then trigger the event.
intervalTimer = NSTimer.scheduledTimerWithTimeInterval(60.0 , target: self, selector: "timerDidEnd:", userInfo: nil, repeats: true)
We’ll place this code into the timerReset
we already have for resetting the timer object
func timerReset(){ // a method to reset timer to 0 and start timer let interval:NSTimeInterval = 5.0 //control the timer control on the interface runTimer.stop() let time = NSDate(timeIntervalSinceNow: interval) runTimer.setDate(time) runTimer.start() // Set up the NSTimer to alert when interval done: intervalTimer = NSTimer.scheduledTimerWithTimeInterval(interval , target: self, selector: "timerDidEnd:", userInfo: nil, repeats: true) }
Note we changed the interval time to 5 seconds for testing purposes.
We have an error. We have not declared intervalTimer
. Add this at the top of the calls with the other variable declarations:
//interval timer var intervalTimer = NSTimer()
To assure we only have one timer running, let’s add the following code above the timer:
if intervalTimer.valid{intervalTimer.invalidate()} //shut off timer if on
NSTimer
has a Bool
property valid
, which indicates the timer is active. If so, we use the invalidate method to shut if off, which also clear memory of the timer. This will clear any active timers, and prevent a possible memory leak. Our current code will need this when we switch manually from run/walk.
We need to make a method for the selector. Add this to your code:
func timerDidEnd(timer:NSTimer){ //when we reach end of an workout interval, switch workout type isRunning = !isRunning runWalkSwitch.setOn(isRunning) runWalkSwitch(isRunning) timerReset() //} }
We toggle isRunning.
With isRunning
in the new state, we use that value to change the switch with the switch’s setOn
method, which also takes a Bool.
Next, we call the method runWalkSwitch,
which will change the state from running to walking or vice-versa in our app. when our state is fully set we invoke timerReset,
which makes a new timer and everything starts again.
We’ll need a way to stop this. add the following method to your code:
func timerStop(){ intervalTimer.invalidate() }
Change to add the timerStop
when we go for Pizza:
@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() timerStop() //new code -- stop the NSTimer } }
Build and run. We now have a working counter
This is not all we can do with NSTimer
. In a separate post, I’ll cover how to use it in a repeat mode to make a full stopwatch / timer in iOS. Our next stop in WatchKit will be separators and sliders.
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 //interval timer var intervalTimer = NSTimer() //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) { //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") } //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() timerStop() } } //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 = 5.0 //Control the timer control on the interface runTimer.stop() let time = NSDate(timeIntervalSinceNow: interval) runTimer.setDate(time) runTimer.start() //control the runLoop timer if intervalTimer.valid{intervalTimer.invalidate()} //shut off timer if on intervalTimer = NSTimer.scheduledTimerWithTimeInterval(interval , target: self, //Object to target when done selector: "timerDidEnd:", //Method on the object userInfo: nil, //Extra user info, most likely a dictionary repeats: false) //Repeat of not } func timerStop(){ intervalTimer.invalidate() } func timerDidEnd(timer:NSTimer){ //When we reach end of an workout interval, switch workout type isRunning = !isRunning runWalkSwitch.setOn(isRunning) runWalkSwitch(isRunning) timerReset() //} } //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