Jolaroux
Jolaroux

Reputation: 361

Is there any way to run code when the time (iOS clock) changes?

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

Answers (2)

Duncan C
Duncan C

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.

EDIT

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.

Edit #2:

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.
    }
  }

Edit #3:

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.)

Edit #4:

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:

  • timeLabel (a UILabel that displays the current time)
  • timeTextView: A UITextView that displays a log of times and suspend/resume events.
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

Jeremy Gurr
Jeremy Gurr

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

Related Questions