Skip to content

Instantly share code, notes, and snippets.

@ZalyalovIldar
Last active August 23, 2018 10:53
Show Gist options
  • Save ZalyalovIldar/a02080141ded4f9de29bd94bc5e7444c to your computer and use it in GitHub Desktop.
Save ZalyalovIldar/a02080141ded4f9de29bd94bc5e7444c to your computer and use it in GitHub Desktop.
/// Timer that can work in background and update timer count if needed even if app resign active
class RepeatingTimer {
fileprivate enum State {
case suspended
case resumed
}
let timeInterval: TimeInterval
/// background queue handler
var eventHandlerOnBackground: (() -> Void)?
/// main queue handler
var eventHandlerOnMain: (() -> Void)?
/// main queue handler trigger when timer finish
var timerFinishEventHandler: (() -> Void)?
/// Timer count in seconds
var timerCount: Int? {
didSet{
shouldContinueTimer = true
guard timerEventHandler == nil else { return }
timerEventHandler = { [weak self] in
guard let strongSelf = self else { return }
guard strongSelf.shouldContinueTimer else {
strongSelf.suspend()
return
}
if let count = strongSelf.timerCount, count <= 0 {
strongSelf.shouldContinueTimer = false
strongSelf.suspend()
DispatchQueue.main.async {
strongSelf.timerFinishEventHandler?()
}
}
else if let count = strongSelf.timerCount {
strongSelf.timerCount = count - Int(strongSelf.timeInterval)
}
}
}
}
fileprivate var notificationCenter: NotificationCenter = NotificationCenter.default
fileprivate var state: State = .suspended
fileprivate var resignActiveDate: Date = Date()
fileprivate var shouldContinueTimer: Bool = true
fileprivate var timerEventHandler: (() -> Void)?
fileprivate lazy var timer: DispatchSourceTimer = {
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
timer.setEventHandler(handler: { [weak self] in
self?.timerEventHandler?()
self?.eventHandlerOnBackground?()
DispatchQueue.main.async { [weak self] in
self?.eventHandlerOnMain?()
}
})
return timer
}()
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
subscribeToAppNotifications()
}
convenience init (timeInterval: TimeInterval, timerCount: Int) {
self.init(timeInterval: timeInterval)
self.timerCount = timerCount
}
deinit {
timer.setEventHandler {}
timer.cancel()
// Can be crash if not resume after cancel while timer was suspended: https://forums.developer.apple.com/thread/15902
resume()
eventHandlerOnBackground = nil
eventHandlerOnMain = nil
timerFinishEventHandler = nil
timerEventHandler = nil
notificationCenter.removeObserver(self)
}
//MARK: Open methods
func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
//MARK: - Private methods
private func subscribeToAppNotifications() {
notificationCenter.addObserver(self, selector: #selector(handleWillResignActiveNotification), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleDidBecomeActiveNotification), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
}
@objc
private func handleWillResignActiveNotification() {
guard timerCount != nil else { return }
suspend()
resignActiveDate = Date()
}
@objc
private func handleDidBecomeActiveNotification() {
guard timerCount != nil else { return }
let deltaInterval = Date().timeIntervalSinceNow - resignActiveDate.timeIntervalSinceNow
timerCount = timerCount! - Int(deltaInterval)
resume()
}
}
Expample usage with timer:
///Need strong reference to timer somewhere outside
var timer = RepeatingTimer(timeInterval: 1)
func setupTimer(with timeCount: Int) {
timer.timerCount = timeCount
timer.eventHandlerOnMain = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.updateTimer(timerCount: strongSelf.timer.timerCount!)
}
timer.timerFinishEventHandler = { [weak self] in
self?.stopTimerAndUpdateTimeLabel()
}
timer.resume()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment