Apple Watch display sleep pauses NSTimer, but WKIn

2019-02-07 10:43发布

问题:

As per Apple documentation, one will use both a WKInterfaceTimer (local to Watch, counts down but does not trigger any event when ends) and an NSTimer (to trigger methods when timer ends). So, I have both an NSTimer and a WKInterfaceTimer in my App Interface Controller. On the Simulator, on all schemes when the WatchApp runs, the NSTimer & the WKInterfaceTimer keep counting down (as they should) when the Watch is either in awake or sleep mode (using the Simulator Lock/Unlock, as instructed in Apple's manual).

However, on the real physical Watch, the 2 timers behave differently upon the Watch display sleep (blackout) and awake states. The sleep mode PAUSES the NSTimer of the Interface Controller, but the WKInterfaceTimer keeps counting down (as it should).

So, the 2 Timers run out of synch immediately upon the first physical Apple Watch sleep (NSTimer pauses, WKInterfaceTimer keeps counting down). Seeking others experiences and whether someone implemented a good way to keep both NSTimer and WKInterfaceTime in synch regardless of the Watch mode (sleep or awake).

回答1:

It seems like you could store the end time of the countdown (for example, in NSUserDefaults), then on willActivate, re-set your NSTimer so that it completes at the correct time. Alternately, you could call your iPhone app to schedule a local notification, but there is no guarantee that the notification will be delivered to your watch, so this might not work for you.



回答2:

The conclusion of my research is that, as per Apple documentation for current release of WatchKit, you will need 2 timers: a WK timer on the Apple Watch and an NSTimer on the iPhone. The 2 timers should start/fire in synch, the WK Timer continues the countdown in both awake/sleep modes, the NSTimer job is to sound the alarm/send notification at timer end time.

To keep both timers in synch, you would need to fire the iPhone NSTimer immediately upon the user starting the Apple Watch WK Timer.



回答3:

Just start/stop the WKInterfaceTimer in willActivate/didDeactivate:

class MyAwesomeInterfaceController: WKInterfaceController {

    @IBOutlet var timer: WKInterfaceTimer!

    override func willActivate() {
        super.willActivate()
        timer.start()
    }

    override func didDeactivate() {
        super.didDeactivate()
        timer.stop()
    }

}


回答4:

When the Apple Watch screen goes blank, it puts the app to sleep and pauses the Timer(s) you had started until the app is brought back into the foreground.
This does not happen in Simulator but does cause a problem on the real device.
The WKInterfaceTimer, however, is not affected because it's based on a future date and handled internally.

So to keep the timer in sync after the app comes back in foreground can be done simply by comparing 2 dates in the Timer block and observing the difference between these 2 dates.

In the following example, if all you want to do is to keep the timer updated and know when the countdown has completed then the following should suffice:

//Globally declared
@IBOutlet var interfaceTimerCountDown: WKInterfaceTimer!

var countdownToDate: Date?

func startCountdown(from count: TimeInterval) {
    //Create a future date to which the countdown will count to
    countdownToDate = Date().addingTimeInterval(count)

    //Set and Start the WKInterfaceTimer
    interfaceTimerCountDown.setDate(countdownToDate!)
    interfaceTimerCountDown.start()

    //Start your own Timer
    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
        //Get current date
        let currentDate = Date()

        //Get difference between future date and current date
        let dxTimeInSeconds = self?.countdownToDate!.timeIntervalSince(currentDate) ?? 0
        print(dxTimeInSeconds)

        //Check if countdown has completed
        if dxTimeInSeconds <= 0 {
            //...do something
            print("Countdown complete!")

            timer.invalidate()
        }
    }
}

But...

The WKInterfaceTimer and Timer will be out of sync by a few milliseconds so if you want to update the UI exactly at the same time when WKInterfaceTimer count has updated then the above logic will not be enough.
In my case, I wanted to update an image; like a ring animation & the only way I got around it was by dumping the WKInterfaceTimer for WKInterfaceLabel + a WKInterfaceGroup and manually updating the label and the group's background image within the timer block.

Custom Solution:

//Declared globally

//for image; to simulate a ring animation image
@IBOutlet var group_lblTimerCount: WKInterfaceGroup!

//Simple label inside a WKInterfaceGroup
@IBOutlet var lblTimerCount: WKInterfaceLabel! //inside group_lblTimerCount

var countdownToDate: Date?

func startCountdown(from count: Int) {
    //Create a future date to which the countdown will count to
    countdownToDate = Date().addingTimeInterval(TimeInterval(count))

    //Update Label and UI
    updateTimerLabel(to: count,
                     from: count)

    //Start your own Timer
    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
        //Get current date
        let currentDate = Date()

        //Get difference between future date and current date
        let dxTimeInSeconds = self?.countdownToDate!.timeIntervalSince(currentDate) ?? 0

        //Update Label and UI
        let dxTimeInSeconds_Int = Int(round(dxTimeInSeconds))
        self?.updateTimerLabel(to: dxTimeInSeconds_Int,
                               from: count)

        //Check if countdown has completed
        if dxTimeInSeconds <= 0 {
            //...do something
            print("Countdown complete!")

            //Stop timer
            timer.invalidate()
        }
    }
}

func updateTimerLabel(to count: Int, from total: Int) {
    lblTimerCount.setText("\(count)")

    updateTimerRing(to: count,
                    from: total)
}

func updateTimerRing(to count: Int, from total: Int) {
    /*
     I have 60 images for the ring named from "ring60" to "ring0"
     Generated at: http://hmaidasani.github.io/RadialChartImageGenerator
     */

    let numberOfImages = 60 //The total number of images you have
    let imageIndex = "\(Int(count * numberOfImages/total))"
    let imageName = "ring\(imageIndex)"

    group_lblTimerCount.setBackgroundImageNamed(imageName)
}

PS: I was trying to find an elegant solution to all of this but couldn't really find a ready example so I am sharing what I ended up with.

Hope it helps someone
:)