For quick 3.0,
Tikhonva's answer does not explain too much. Something from my understanding is added here.
To make things short, here is the code. This is DIFFERENT from Tikhonov's code in the place where I create the timer. I create a timer using the constructor and add it to the loop. I think the schedTimer function will add a timer to the main RunLoop thread. Therefore, it is better to create a timer using the constructor.
class RunTimer{ let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent) let timer: Timer? private func startTimer() { // schedule timer on background queue.async { [unowned self] in if let _ = self.timer { self.timer?.invalidate() self.timer = nil } let currentRunLoop = RunLoop.current self.timer = Timer(timeInterval: self.updateInterval, target: self, selector:
Create queue
First create a queue to start the timer in the background and save this queue as a class property to reuse it for the stop timer. I'm not sure that we need to use the same queue to start and stop, so I did this because I saw a warning message here here .
The RunLoop class is usually not considered thread safe and its methods should only be called in the context of the current thread. You should never try to call methods on an object. RunLoop is running in another thread, as this may lead to unexpected results.
Therefore, I decided to save the queue and use the same queue for the timer to avoid synchronization problems.
Also create an empty timer and save it in a class variable. Make this optional to stop the timer and set it to zero.
class RunTimer{ let queue = DispatchQueue(label: "Timer", qos: .background, attributes: .concurrent) let timer: Timer? }
Start timer
To start the timer, first call async from the DispatchQueue. Then itβs better to check if the timer is already running. If the timer variable is not nil, then invalidate () and set it to zero.
The next step is to get the current RunLoop. Since we did this in the created queue block, it will get RunLoop for the previously created background queue.
Create a timer. Here, instead of using schedTimer, we simply call the timer constructor and pass in any property that you want for the timer, for example timeInterval, target, selector, etc.
Add the created timer to RunLoop. Run it.
Here is a question about running RunLoop. According to the documentation here it says that it actually starts an infinite loop that processes data from input sources and timers.
private func startTimer() { // schedule timer on background queue.async { [unowned self] in if let _ = self.timer { self.timer?.invalidate() self.timer = nil } let currentRunLoop = RunLoop.current self.timer = Timer(timeInterval: self.updateInterval, target: self, selector:
Trigger timer
Implement the function as usual. When this function is called, it is called in the default queue.
func timerTriggered() { // under queue by default debug() } func debug() { let name = __dispatch_queue_get_label(nil) print(String(cString: name, encoding: .utf8)) }
The debugging function above is used to print the queue name. If you ever worried if he was running in line, you can call him to check.
Stop timer
The break timer is simple, call validate () and set the timer variable stored inside the class to zero.
Here I run it again under the queue. Due to a warning here, I decided to run all the timer related code in the queue to avoid conflicts.
func stopTimer() { queue.sync { [unowned self] in guard let _ = self.timer else { // error, timer already stopped return } self.timer?.invalidate() self.timer = nil } }
Questions related to RunLoop
I'm a little bit confused if we need to manually stop RunLoop or not. According to the documentation here, it seems that when timers are not connected to it, it will exit immediately. Therefore, when we stop the timer, it must exist by itself. However, at the end of this document, he also said:
Removing all known input sources and timers from the startup cycle is not a sure that the execution cycle is complete. macOS can install and remove additional input sources needed to process thread-oriented requests. Thus, these sources can interfere with the start-up cycle from the output.
I tried the solution below provided in the documentation to guarantee the completion of the cycle. However, the timer does not fire after I changed .run () to the code below.
while (self.timer != nil && currentRunLoop.run(mode: .commonModes, before: Date.distantFuture)) {};
I think it can only be safe to use .run () on iOS. Since the documentation states that macOS installs and removes additional input sources as needed to handle requests aimed at the recipient stream. So iOS might be fine.