Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Last active April 21, 2022 18:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brennanMKE/1dbc83483d527491ab96624155d0b7fe to your computer and use it in GitHub Desktop.
Save brennanMKE/1dbc83483d527491ab96624155d0b7fe to your computer and use it in GitHub Desktop.
Timer with Dispatch in Swift

Timer with Dispatch in Swift

Running a work item after a timer expires can be done with many legacy techniques such as performing a selector after a delay. The modern Dispatch framework should be used instead of these outdated techniques which use the Objective-C runtime. This code defines DispatchTimer which supports running a work item after the timer expires. It can be invalidated so that the work item is not executed.

See the tests for examples of how to use it.

import Foundation
public protocol AnyTimer: AnyObject {
static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, queue: DispatchQueue, block: @escaping (AnyTimer) -> Void) -> AnyTimer
init(withTimeInterval interval: TimeInterval, repeats: Bool, queue: DispatchQueue, block: @escaping (AnyTimer) -> Void)
func invalidate()
var isValid: Bool { get }
}
public class DispatchTimer: AnyTimer {
private enum DispatchTimerState {
case valid(DispatchWorkItem)
case invalid
}
private let interval: TimeInterval
private let repeats: Bool
private let queue: DispatchQueue
private let block: (AnyTimer) -> Void
private var state: DispatchTimerState = .invalid
private var workItem: DispatchWorkItem? {
let result: DispatchWorkItem?
switch state {
case .valid(let workItem):
result = workItem
case .invalid:
result = nil
}
return result
}
public var isValid: Bool {
let result: Bool
switch state {
case .valid:
result = true
case .invalid:
result = false
}
return result
}
public required init(withTimeInterval interval: TimeInterval, repeats: Bool = false, queue: DispatchQueue = .global(), block: @escaping (AnyTimer) -> Void) {
self.interval = interval
self.repeats = repeats
self.queue = queue
self.block = block
scheduleTimer()
}
public static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool = false, queue: DispatchQueue = .global(), block: @escaping (AnyTimer) -> Void) -> AnyTimer {
let timer = DispatchTimer(withTimeInterval: interval, repeats: repeats, block: block)
return timer
}
public func invalidate() {
workItem?.cancel()
state = .invalid
}
private func scheduleTimer() {
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.fireTimer()
}
DispatchQueue.global().asyncAfter(deadline: .now() + interval, execute: workItem)
state = .valid(workItem)
}
private func fireTimer() {
queue.async { [weak self] in
guard let self = self else { return }
self.block(self)
}
// run after the queue has completed all other work
queue.sync(flags: .barrier) {
if !repeats {
state = .invalid
} else if isValid {
scheduleTimer()
}
}
}
}
import XCTest
class DispatchTimerTests: XCTestCase {
func testDispatchTimerRunsOnce() {
var count = 0
let queue = DispatchQueue(label: #function)
let exp = expectation(description: #function)
var timer: AnyTimer? = DispatchTimer(withTimeInterval: 0.1, repeats: false, queue: queue) { _ in
print("tick")
count += 1
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
exp.fulfill()
}
wait(for: [exp], timeout: 10.0)
print("count = \(count) [\(#function)]")
XCTAssertEqual(count, 1)
timer?.invalidate()
timer = nil
}
func testDispatchTimerSetToNil() {
var count = 0
let queue = DispatchQueue(label: #function)
let exp = expectation(description: #function)
var timer: AnyTimer? = DispatchTimer(withTimeInterval: 0.1, repeats: false, queue: queue) { _ in
print("tick")
count += 1
}
timer = nil
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
exp.fulfill()
}
wait(for: [exp], timeout: 10.0)
print("count = \(count) [\(#function)]")
XCTAssertEqual(count, 0)
timer?.invalidate()
timer = nil
}
func testDispatchTimerRepeats() {
var count = 0
let queue = DispatchQueue(label: #function)
let exp = expectation(description: #function)
var timer: AnyTimer? = DispatchTimer(withTimeInterval: 0.1, repeats: true, queue: queue) { timer in
print("tick")
count += 1
if count >= 2 {
timer.invalidate()
exp.fulfill()
}
}
wait(for: [exp], timeout: 10.0)
print("count = \(count) [\(#function)]")
XCTAssertGreaterThan(count, 1)
XCTAssertFalse(timer?.isValid ?? false)
timer?.invalidate()
timer = nil
}
func testDispatchTimerRunsThreeTimes() {
var count = 0
let queue = DispatchQueue(label: #function)
let exp = expectation(description: #function)
var timer: AnyTimer? = DispatchTimer(withTimeInterval: 0.1, repeats: true, queue: queue) { timer in
print("tick")
count += 1
if count >= 3 {
timer.invalidate()
exp.fulfill()
}
}
wait(for: [exp], timeout: 10.0)
XCTAssertNotNil(timer)
let valid = timer?.isValid ?? false
print("valid: \(valid)")
print("count = \(count) [\(#function)]")
XCTAssertGreaterThan(count, 1)
XCTAssertFalse(valid)
timer?.invalidate()
timer = nil
}
func testDispatchTimerIsCanceled() {
var count = 0
let queue = DispatchQueue(label: #function)
let exp = expectation(description: #function)
var timer: AnyTimer? = DispatchTimer(withTimeInterval: 0.1, repeats: false, queue: queue) { _ in
print("tick")
count += 1
}
timer?.invalidate()
DispatchQueue.global().asyncAfter(deadline: .now() + 0.25) {
exp.fulfill()
}
wait(for: [exp], timeout: 10.0)
print("count = \(count) [\(#function)]")
XCTAssertEqual(count, 0)
XCTAssertFalse(timer?.isValid ?? false)
timer?.invalidate()
timer = nil
}
func testDispatchTimerCreatedWithStaticInitializer() {
var count = 0
let queue = DispatchQueue(label: #function)
let exp = expectation(description: #function)
var timer: AnyTimer? = DispatchTimer.scheduledTimer(withTimeInterval: 0.1, repeats: false, queue: queue) { _ in
print("tick")
count += 1
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
exp.fulfill()
}
wait(for: [exp], timeout: 10.0)
print("count = \(count) [\(#function)]")
XCTAssertEqual(count, 1)
timer?.invalidate()
timer = nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment