Swift WatchKit Tutorial: Coding Timers and NSTimer on Apple Watch

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

NStimer working

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()
    }

}

10 thoughts on “Swift WatchKit Tutorial: Coding Timers and NSTimer on Apple Watch”

  1. My understanding is that you can’t really have background apps run on the watch. So the utility of an NSTimer item is questionable, no?

      1. This isn’t quite accurate as you can run NSTimer without connectivity to the phone.

        That said when the watch sleeps NSTimer appears to sleep with it so the utility is quite small.

      2. I’ve been back and forth on this in the comments. Your statement is the most accurate. I’m hoping for different, betterbehavior in whatever the next WatchOS iteration has.

  2. The time runs on the watch not the phone. Also when the watch is sleep it stops the timer. So this app will not work as expected. You’ll need to use a workoutsession to make this work.

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s