Reputation: 361
Is there anyway to run code when the system clock changes?
For example: when the clock increments by one minute, run print(systemtime)
, the variable that I store the current time in.
This is just for my own testing purposes, right now I have a timer that repeats every minute, gets the current time, and prints it.
However, I'm looking for a way to print
the time to the console at the exact time it changes, in iOS.
In a more general sense, a way to trigger running code when the iOS clock changes.
Upvotes: 1
Views: 1319
Reputation: 131511
In a word, no. The realtime clock on iOS has sub-microsecond precision. It uses a double to report the time in fractional seconds since the iOS "epoch date".
Calling a function on some arbitrary interval (60 seconds, on the second) is not a system function.
That said, you could set up a CADisplayLink
(a low overhead timer) synchronized to the screen refresh, and when the time is within some threshold of a minute change" (fmod(NSdate.timeIntervalSinceReferenceDate, 60) < 0.02
, say) you could log your message to the console. You'd need to use a threshold because any interval you set up may not occur exactly on the desired mark.
As Matt points out in the comments below, a CADisplayLink
would likely cause the device CPU to "run hot" and drain the battery faster than desired.
If you don't need millisecond accuracy then doing some math to calculate the next minute interveral and starting a repeating timer after an appropriate delay is probably a good compromise. Something like this:
var timer: Timer?
func startMinuteTimer() {
let now = Date.timeIntervalSinceReferenceDate
let delayFraction = trunc(now) - now
//Caluclate a delay until the next even minute
let delay = 60.0 - Double(Int(now) % 60) + delayFraction
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
timer in
//Put your code that runs every minute here.
}
}
}
The resolution of NSTimer (Timer, in Swift 3) is supposed to be around 1/50 of a second, so that code should get your updates to within about 0.02 seconds of your target time.
Also, any time your app becomes inactive and then active again you'll have to kill the above timer and restart it since it will be paused while your app isn't running and it won't be synced to fire on even minutes any more.
As pointed out in the comments to my answer, the code above would fail to fire on the first even minute after it starts executing. Assuming you have a function timeFunction in your class that you want to run every minute, you could fix that like this:
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.timeFunction() //Call your code that runs every minute the first time
self.timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
timer in
self.timeFunction() //Call your code that runs every minute.
}
}
I used the above code in a sample iOS app, and found that if you suspend the app (swap it to the background) then the timer stops firing, as expected. When you resume your app, the timer fires almost immediately, out of sync with the "on-the-minute" time. It then first again on the next on-the-minute time.
In order to avoid the off-time call when you get a resume call you would have to listen for suspend and resume events, invalidate your timer on suspend events, and re-create your timer it on resume events. (Without setting up your app to run in the background, there's no way to get it to keep firing every minute when it's in the background or the phone is locked however.)
Here is a the view controller code from a working "single view" app that does everything described: Adds observers for the willResignActiveNotification and didBecomeActiveNotification, starts a single-shot timer to synchronize with the change of minutes, and then a repeating timer every minute. It has code to display the time to a label every time the timer fires, and also append the time to a text view every time it updates so you can see the behavior, and log suspend/resume events.
It has 2 outlets:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var timeTextView: UITextView!
weak var timer: Timer?
var observer1, observer2: Any?
lazy var timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm.ss.SSS"
return formatter
}()
func appendStringToLog(string: String) {
let newTimeLogText = timeTextView.text + "\n" + string
timeTextView.text = newTimeLogText
}
func timeFunction() {
let timeString = timeFormatter.string(from: Date())
timeLabel.text = timeString
appendStringToLog(string: timeString)
print(timeString)
}
func startMinuteTimer() {
let now = Date.timeIntervalSinceReferenceDate
let delayFraction = trunc(now) - now
//Caluclate a delay until the next even minute
let delay = 60.0 - Double(Int(now) % 60) + delayFraction
//First use a one-shot timer to wait until we are on an even minute interval
self.timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { timer in
//The first time through, call the time function
self.timeFunction() //Call your code that runs every minute the first time
//Now create a repeating timer that fires once a minute.
self.timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
timer in
self.timeFunction() //Call your code that runs every minute.
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
//Observe the willResignActiveNotification so we can kill our timer
observer1 = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { notification in
let string = "Suspending. Stopping timer at \(self.timeFormatter.string(from: Date()))"
print(string)
self.appendStringToLog(string: string)
self.timer?.invalidate()
}
//Observe the didBecomeActiveNotification so we can start our timer (gets called on app launch and on resume)
observer2 = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { notification in
let string = "Becoming active. Starting timer at \(self.timeFormatter.string(from: Date()))"
print(string)
self.appendStringToLog(string: string)
self.startMinuteTimer()
}
}
}
Upvotes: 5
Reputation: 1623
I don't believe there is any mechanism to create a trigger on the system clock, but you can get the same effect by checking the system clock when your app starts, and starting a timer that will go off when the system clock would have changed minutes. For example, if the time is 4:30:50, then you can start a timer that triggers after 10 seconds, which will happen very close to 4:31:00. And then when it triggers, you just immediately start another timer for 1 minute later.
Upvotes: 0