You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
397 lines
13 KiB
397 lines
13 KiB
6 years ago
|
// Copyright © 2015 Abhishek Banthia
|
||
|
|
||
|
import Foundation
|
||
|
|
||
|
open class Repeater: Equatable {
|
||
|
/// State of the timer
|
||
|
///
|
||
|
/// - paused: idle (never started yet or paused)
|
||
|
/// - running: timer is running
|
||
|
/// - executing: the observers are being executed
|
||
|
/// - finished: timer lifetime is finished
|
||
|
public enum State: Equatable, CustomStringConvertible {
|
||
|
case paused
|
||
|
case running
|
||
|
case executing
|
||
|
case finished
|
||
|
|
||
|
public static func == (lhs: State, rhs: State) -> Bool {
|
||
|
switch (lhs, rhs) {
|
||
|
case (.paused, .paused),
|
||
|
(.running, .running),
|
||
|
(.executing, .executing),
|
||
|
(.finished, .finished):
|
||
|
return true
|
||
|
default:
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Return `true` if timer is currently running, including when the observers are being executed.
|
||
|
public var isRunning: Bool {
|
||
|
guard self == .running || self == .executing else { return false }
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
/// Return `true` if the observers are being executed.
|
||
|
public var isExecuting: Bool {
|
||
|
guard case .executing = self else { return false }
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
/// Is timer finished its lifetime?
|
||
|
/// It return always `false` for infinite timers.
|
||
|
/// It return `true` for `.once` mode timer after the first fire,
|
||
|
/// and when `.remainingIterations` is zero for `.finite` mode timers
|
||
|
public var isFinished: Bool {
|
||
|
guard case .finished = self else { return false }
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
/// State description
|
||
|
public var description: String {
|
||
|
switch self {
|
||
|
case .paused: return "idle/paused"
|
||
|
case .finished: return "finished"
|
||
|
case .running: return "running"
|
||
|
case .executing: return "executing"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Repeat interval
|
||
|
public enum Interval {
|
||
|
case nanoseconds(_: Int)
|
||
|
case microseconds(_: Int)
|
||
|
case milliseconds(_: Int)
|
||
|
case minutes(_: Int)
|
||
|
case seconds(_: Double)
|
||
|
case hours(_: Int)
|
||
|
case days(_: Int)
|
||
|
|
||
|
internal var value: DispatchTimeInterval {
|
||
|
switch self {
|
||
|
case let .nanoseconds(value): return .nanoseconds(value)
|
||
|
case let .microseconds(value): return .microseconds(value)
|
||
|
case let .milliseconds(value): return .milliseconds(value)
|
||
|
case let .seconds(value): return .milliseconds(Int(Double(value) * Double(1000)))
|
||
|
case let .minutes(value): return .seconds(value * 60)
|
||
|
case let .hours(value): return .seconds(value * 3600)
|
||
|
case let .days(value): return .seconds(value * 86400)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Mode of the timer.
|
||
|
///
|
||
|
/// - infinite: infinite number of repeats.
|
||
|
/// - finite: finite number of repeats.
|
||
|
/// - once: single repeat.
|
||
|
public enum Mode {
|
||
|
case infinite
|
||
|
case finite(_: Int)
|
||
|
case once
|
||
|
|
||
|
/// Is timer a repeating timer?
|
||
|
internal var isRepeating: Bool {
|
||
|
switch self {
|
||
|
case .once: return false
|
||
|
default: return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Number of repeats, if applicable. Otherwise `nil`
|
||
|
public var countIterations: Int? {
|
||
|
switch self {
|
||
|
case let .finite(counts): return counts
|
||
|
default: return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Is infinite timer
|
||
|
public var isInfinite: Bool {
|
||
|
guard case .infinite = self else {
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Handler typealias
|
||
|
public typealias Observer = ((Repeater) -> Void)
|
||
|
|
||
|
/// Token assigned to the observer
|
||
|
public typealias ObserverToken = UInt64
|
||
|
|
||
|
/// Current state of the timer
|
||
|
public private(set) var state: State = .paused {
|
||
|
didSet {
|
||
|
self.onStateChanged?(self, state)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Callback called to intercept state's change of the timer
|
||
|
public var onStateChanged: ((_ timer: Repeater, _ state: State) -> Void)?
|
||
|
|
||
|
/// List of the observer of the timer
|
||
|
private var observers = [ObserverToken: Observer]()
|
||
|
|
||
|
/// Next token of the timer
|
||
|
private var nextObserverID: UInt64 = 0
|
||
|
|
||
|
/// Internal GCD Timer
|
||
|
private var timer: DispatchSourceTimer?
|
||
|
|
||
|
/// Is timer a repeat timer
|
||
|
public private(set) var mode: Mode
|
||
|
|
||
|
/// Number of remaining repeats count
|
||
|
public private(set) var remainingIterations: Int?
|
||
|
|
||
|
/// Interval of the timer
|
||
|
private var interval: Interval
|
||
|
|
||
|
/// Accuracy of the timer
|
||
|
private var tolerance: DispatchTimeInterval
|
||
|
|
||
|
/// Dispatch queue parent of the timer
|
||
|
private var queue: DispatchQueue?
|
||
|
|
||
|
/// Initialize a new timer.
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - interval: interval of the timer
|
||
|
/// - mode: mode of the timer
|
||
|
/// - tolerance: tolerance of the timer, 0 is default.
|
||
|
/// - queue: queue in which the timer should be executed; if `nil` a new queue is created automatically.
|
||
|
/// - observer: observer
|
||
|
public init(interval: Interval, mode: Mode = .infinite, tolerance: DispatchTimeInterval = .nanoseconds(3), queue: DispatchQueue? = nil, observer: @escaping Observer) {
|
||
|
self.mode = mode
|
||
|
self.interval = interval
|
||
|
self.tolerance = tolerance
|
||
|
remainingIterations = mode.countIterations
|
||
|
self.queue = (queue ?? DispatchQueue(label: "com.abhishek.Clocker"))
|
||
|
timer = configureTimer()
|
||
|
observe(observer)
|
||
|
}
|
||
|
|
||
|
/// Add new a listener to the timer.
|
||
|
///
|
||
|
/// - Parameter callback: callback to call for fire events.
|
||
|
/// - Returns: token used to remove the handler
|
||
|
@discardableResult
|
||
|
public func observe(_ observer: @escaping Observer) -> ObserverToken {
|
||
|
var (new, overflow) = nextObserverID.addingReportingOverflow(1)
|
||
|
if overflow { // you need to add an incredible number of offset...sure you can't
|
||
|
nextObserverID = 0
|
||
|
new = 0
|
||
|
}
|
||
|
nextObserverID = new
|
||
|
observers[new] = observer
|
||
|
return new
|
||
|
}
|
||
|
|
||
|
/// Remove an observer of the timer.
|
||
|
///
|
||
|
/// - Parameter id: id of the observer to remove
|
||
|
public func remove(observer identifier: ObserverToken) {
|
||
|
observers.removeValue(forKey: identifier)
|
||
|
}
|
||
|
|
||
|
/// Remove all observers of the timer.
|
||
|
///
|
||
|
/// - Parameter stopTimer: `true` to also stop timer by calling `pause()` function.
|
||
|
public func removeAllObservers(thenStop stopTimer: Bool = false) {
|
||
|
observers.removeAll()
|
||
|
|
||
|
if stopTimer {
|
||
|
pause()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Configure a new timer session.
|
||
|
///
|
||
|
/// - Returns: dispatch timer
|
||
|
private func configureTimer() -> DispatchSourceTimer {
|
||
|
let associatedQueue = (queue ?? DispatchQueue(label: "com.repeat.\(NSUUID().uuidString)"))
|
||
|
let timer = DispatchSource.makeTimerSource(queue: associatedQueue)
|
||
|
let repeatInterval = interval.value
|
||
|
let deadline: DispatchTime = (DispatchTime.now() + repeatInterval)
|
||
|
if mode.isRepeating {
|
||
|
timer.schedule(deadline: deadline, repeating: repeatInterval, leeway: tolerance)
|
||
|
} else {
|
||
|
timer.schedule(deadline: deadline, leeway: tolerance)
|
||
|
}
|
||
|
|
||
|
timer.setEventHandler { [weak self] in
|
||
|
if let unwrapped = self {
|
||
|
unwrapped.timeFired()
|
||
|
}
|
||
|
}
|
||
|
return timer
|
||
|
}
|
||
|
|
||
|
/// Destroy current timer
|
||
|
private func destroyTimer() {
|
||
|
timer?.setEventHandler(handler: nil)
|
||
|
timer?.cancel()
|
||
|
|
||
|
if state == .paused || state == .finished {
|
||
|
timer?.resume()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Create and schedule a timer that will call `handler` once after the specified time.
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - interval: interval delay for single fire
|
||
|
/// - queue: destination queue, if `nil` a new `DispatchQueue` is created automatically.
|
||
|
/// - observer: handler to call when timer fires.
|
||
|
/// - Returns: timer instance
|
||
|
@discardableResult
|
||
|
public class func once(after interval: Interval, queue: DispatchQueue? = nil, _ observer: @escaping Observer) -> Repeater {
|
||
|
let timer = Repeater(interval: interval, mode: .once, queue: queue, observer: observer)
|
||
|
timer.start()
|
||
|
return timer
|
||
|
}
|
||
|
|
||
|
/// Create and schedule a timer that will fire every interval optionally by limiting the number of fires.
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - interval: interval of fire
|
||
|
/// - count: a non `nil` and > 0 value to limit the number of fire, `nil` to set it as infinite.
|
||
|
/// - queue: destination queue, if `nil` a new `DispatchQueue` is created automatically.
|
||
|
/// - handler: handler to call on fire
|
||
|
/// - Returns: timer
|
||
|
@discardableResult
|
||
|
public class func every(_ interval: Interval, count: Int? = nil, queue: DispatchQueue? = nil, _ handler: @escaping Observer) -> Repeater {
|
||
|
let mode: Mode = (count != nil ? .finite(count!) : .infinite)
|
||
|
let timer = Repeater(interval: interval, mode: mode, queue: queue, observer: handler)
|
||
|
timer.start()
|
||
|
return timer
|
||
|
}
|
||
|
|
||
|
/// Force fire.
|
||
|
///
|
||
|
/// - Parameter pause: `true` to pause after fire, `false` to continue the regular firing schedule.
|
||
|
public func fire(andPause pause: Bool = false) {
|
||
|
timeFired()
|
||
|
if pause == true {
|
||
|
self.pause()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Reset the state of the timer, optionally changing the fire interval.
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - interval: new fire interval; pass `nil` to keep the latest interval set.
|
||
|
/// - restart: `true` to automatically restart the timer, `false` to keep it stopped after configuration.
|
||
|
public func reset(_ interval: Interval?, restart: Bool = true) {
|
||
|
if state.isRunning {
|
||
|
setPause(from: state)
|
||
|
}
|
||
|
|
||
|
// For finite counter we want to also reset the repeat count
|
||
|
if case let .finite(count) = mode {
|
||
|
self.remainingIterations = count
|
||
|
}
|
||
|
|
||
|
// Create a new instance of timer configured
|
||
|
if let newInterval = interval {
|
||
|
self.interval = newInterval
|
||
|
} // update interval
|
||
|
destroyTimer()
|
||
|
timer = configureTimer()
|
||
|
state = .paused
|
||
|
|
||
|
if restart {
|
||
|
timer?.resume()
|
||
|
state = .running
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Start timer. If timer is already running it does nothing.
|
||
|
@discardableResult
|
||
|
public func start() -> Bool {
|
||
|
guard state.isRunning == false else {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// If timer has not finished its lifetime we want simply
|
||
|
// restart it from the current state.
|
||
|
guard state.isFinished == true else {
|
||
|
state = .running
|
||
|
timer?.resume()
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// Otherwise we need to reset the state based upon the mode
|
||
|
// and start it again.
|
||
|
reset(nil, restart: true)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
/// Pause a running timer. If timer is paused it does nothing.
|
||
|
@discardableResult
|
||
|
public func pause() -> Bool {
|
||
|
guard state != .paused && state != .finished else {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return setPause(from: state)
|
||
|
}
|
||
|
|
||
|
/// Pause a running timer optionally changing the state with regard to the current state.
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - from: the state which the timer should only be paused if it is the current state
|
||
|
/// - to: the new state to change to if the timer is paused
|
||
|
/// - Returns: `true` if timer is paused
|
||
|
@discardableResult
|
||
|
private func setPause(from currentState: State, to newState: State = .paused) -> Bool {
|
||
|
guard state == currentState else {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
timer?.suspend()
|
||
|
state = newState
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
/// Called when timer is fired
|
||
|
private func timeFired() {
|
||
|
state = .executing
|
||
|
|
||
|
// dispatch to observers
|
||
|
observers.values.forEach { $0(self) }
|
||
|
|
||
|
// manage lifetime
|
||
|
switch mode {
|
||
|
case .once:
|
||
|
// once timer's lifetime is finished after the first fire
|
||
|
// you can reset it by calling `reset()` function.
|
||
|
setPause(from: .executing, to: .finished)
|
||
|
case .finite:
|
||
|
// for finite intervals we decrement the left iterations count...
|
||
|
remainingIterations! -= 1
|
||
|
if remainingIterations! == 0 {
|
||
|
// ...if left count is zero we just pause the timer and stop
|
||
|
setPause(from: .executing, to: .finished)
|
||
|
}
|
||
|
case .infinite:
|
||
|
// infinite timer does nothing special on the state machine
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
deinit {
|
||
|
self.observers.removeAll()
|
||
|
self.destroyTimer()
|
||
|
}
|
||
|
|
||
|
public static func == (lhs: Repeater, rhs: Repeater) -> Bool {
|
||
|
return lhs === rhs
|
||
|
}
|
||
|
}
|