Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A repeating GCD timer that can run on a background queue
/// RepeatingTimer mimics the API of DispatchSourceTimer but in a way that prevents
/// crashes that occur from calling resume multiple times on a timer that is
/// already resumed (noted by https://github.com/SiftScience/sift-ios/issues/52
class RepeatingTimer {
let timeInterval: TimeInterval
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
timer.setEventHandler {}
timer.cancel()
/*
If the timer is suspended, calling cancel without resuming
triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
*/
resume()
eventHandler = nil
}
func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
}
@scottymac

This comment has been minimized.

Copy link

@scottymac scottymac commented Nov 6, 2017

Daniel - is this ok to use in a project? -- permission-wise, I mean

@danielgalasko

This comment has been minimized.

Copy link
Owner Author

@danielgalasko danielgalasko commented May 7, 2018

totally fine @scottymac I was not sure how to license snippets but this is definitely code you can use under your own discretion :)

@rodrigo98rm

This comment has been minimized.

Copy link

@rodrigo98rm rodrigo98rm commented Jan 20, 2019

Hi @danielgalasko,

I'm trying to use this class but something weird is happening.

I used this bit of code from your Medium article, but nothing was printed to the console:

let t = RepeatingTimer(timeInterval: 3)
        t.eventHandler = {
            print("Timer Fired")
        }
        t.resume()

But, (now comes the weird part) if I do this, then "Timer Fired" gets printed every 3 seconds:

let t = RepeatingTimer(timeInterval: 3)
        t.eventHandler = {
            print("Timer Fired")
            if(false){   //I know this makes no sense, but it works. Go figure...
                t.suspend()
            }
        }
        t.resume()

Any ideas?

@berkakkerman

This comment has been minimized.

Copy link

@berkakkerman berkakkerman commented Apr 16, 2019

"eventHandler" never triggers. What am I missing? Is there a issue with capabilities of app or configuration.

@sdebaene

This comment has been minimized.

Copy link

@sdebaene sdebaene commented Apr 28, 2019

@rodrigo98rm It's because your timer 't' has been eaten by garbage collector before even starting. In your weird scenario, it's because your handler keeps a reference to 't' so the garbage collector don't eat it. You are probably running the test inside a func.

@Deco354

This comment has been minimized.

Copy link

@Deco354 Deco354 commented Jun 17, 2019

withExtendedLifetime(::) should keep it alive if you want to keep it as a local property.

Setting it to an instance property should also do the trick, but be sure to handle the retain cycle if you reference self inside the closure.

@Haibo-Zhou

This comment has been minimized.

Copy link

@Haibo-Zhou Haibo-Zhou commented Oct 26, 2019

Could I add Selector in your customer timer? like Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self.trackAudio), userInfo: nil, repeats: true), because I need to track audio process and update app's progress bar in the selector method. Currently, timer doesn't resume from background, so I want to try yours custom timer.

@Creanomy

This comment has been minimized.

Copy link

@Creanomy Creanomy commented Feb 2, 2020

Hi @danielgalasko,
I have read your article in Medium with attention and was interested in using your class for a personal project. I tested it to display a background timer in a kind of game screen. This way, user can see elapsed time on the main screen and meanwhile, he can go through other VC without stopping timer.

However, a problem occurs :
For example, I store in CoreData the value for time elapsed (counter) :
var counter: Int16? var t = RepeatingTimer(timeInterval: 1)
In the viewDidLoad :
t.eventHandler = { self.counter! += 1 print("counter \(String(describing: self.counter!))") self.myEntity?.timeAsAttribute = self.counter ?? 0 self.save() DispatchQueue.main.async { self.timerField.text = String(describing: self.counter ?? 0) // label on my main screen } }
If I suspend() the timer, go to other VC, come back to my main screen, and resume() : everything is ok, my label (texfield) is actualized with my timeInterval.
But when, I go to other VC and come back without suspend() the timer, I can see my print func working, but label is "freezing" on the last known value before coming back, and my button play/Pause doesn't work... Unable to make it work correctly.
Any idea about what I did wrong ?
I'm juste like my label, I'm freezed on my func.

@iSachdeva

This comment has been minimized.

Copy link

@iSachdeva iSachdeva commented Mar 2, 2020

Hi @danielgalasko,

I'm trying to use this class but something weird is happening.

I used this bit of code from your Medium article, but nothing was printed to the console:

let t = RepeatingTimer(timeInterval: 3)
        t.eventHandler = {
            print("Timer Fired")
        }
        t.resume()

But, (now comes the weird part) if I do this, then "Timer Fired" gets printed every 3 seconds:

let t = RepeatingTimer(timeInterval: 3)
        t.eventHandler = {
            print("Timer Fired")
            if(false){   //I know this makes no sense, but it works. Go figure...
                t.suspend()
            }
        }
        t.resume()

Any ideas?

variable t should be a class variable instead of local. And it will work.

@iSachdeva

This comment has been minimized.

Copy link

@iSachdeva iSachdeva commented Mar 2, 2020

This is not working when app goes in background. Any idea on resolution? Please help.

@Streebor

This comment has been minimized.

Copy link

@Streebor Streebor commented Jul 27, 2020

This is not working when app goes in background. Any idea on resolution? Please help.

As far as I know, timers do not work in the background. What do you user your timer for?

If you have a stopwatch type of UI where your time needs to update when user reopens the app, try saving a timestamp when the app is backgrounded. Then, when app is foregrounded again by the user, calculate the diff in time and update your UI and restart your timer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.