Apple Watch display indicator pauses NSTimer, but WKInterfaceTimer keeps counting
According to Apple's documentation, you will use both WKInterfaceTimer (local to Watch, counting down, but not triggering any event on completion) and NSTimer (to trigger methods when the timer ends). So, I have both NSTimer and WKInterfaceTimer in my Application Interface Controller. On the simulator, in all circuits when the WatchApp is running, the NSTimer and WKInterfaceTimer continue to count down (as they should) when the Watch is either idle or idle (using the simulator lock / unlock as specified in the Apple manual).
However, on a real physical Watch, the 2 timers behave differently in display standby (dimmed) and wakeup states. Hibernate PAUSES NSTimer of the interface controller, but WKInterfaceTimer continues to count down (as it should).
So, the two timers don't work immediately after the first physical sleep of the Apple Watch (NSTimer pauses, WKInterfaceTimer continues to count). Looking for other experiences and has anyone implemented a good way to sync both NSTimer and WKInterfaceTime regardless of Watch mode (sleep or awake).
source to share
It seems that you can save the countdown end time (like in NSUserDefaults
), then willActivate
re-install the NSTimer so that it ends at the right time. Alternatively, you can 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 may not work for you.
source to share
The end of my research is that according to Apple documentation for the current version of WatchKit, you will need 2 timers: the WK timer on the Apple Watch and the NSTimer on the iPhone. 2 timers should start / start synchronously, WK Timer keeps counting down as in standby / sleep mode, NSTimer job should trigger alarm / dispatch notification when timer expires.
To sync both timers, you need to start the iPhone NSTimer immediately when the user starts the Watch Apple Watch WK timer.
source to share
Just start / stop 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()
}
}
source to share
When the Apple Watch screen disappears, the app will deactivate and suspend Timer
(s) that you launched until the app is brought back to the foreground.
This does not happen in the Simulator, but it does cause a problem on a real device.
However, it is WKInterfaceTimer
not affected because it is based on a future date and is handled internally.
So, to keep the timer in sync after the app is brought back to the foreground can be done simply by comparing the 2 dates in the block Timer
and observing the difference between the two dates.
In the following example, if all you want to do is keep the timer updated and know when the countdown has finished, the following should be sufficient:
//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...
WKInterfaceTimer
and Timer
will be out of sync for a few milliseconds, so if you want to update the user interface exactly at the same time, when the counter WKInterfaceTimer
is updated, the logic is not enough.
In my case, I wanted to update the image; like a ring animation and the only way I have communicated is to reset WKInterfaceTimer
for WKInterfaceLabel
+ a WKInterfaceGroup
and manually update the label and background image of the group in 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 tried to find an elegant solution for all this, but could not find a suitable example, so I am sharing what I got.
Hope this helps someone :)
source to share