forked from sideeffect-io/Regulate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Debouncer.swift
151 lines (129 loc) · 4.38 KB
/
Debouncer.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//
// Debouncer.swift
//
//
// Created by Thibault Wittemberg on 28/09/2022.
//
import Foundation
public extension Task where Failure == Never {
/// Creates a `Regulator` that executes an output only after a specified time interval elapses between events
/// - Parameters:
/// - dueTime: the time the Debouncer should wait before executing the output
/// - output: the block to execute once the regulation is done
/// - Returns: the debounced regulator
static func debounce(
dueTime: DispatchTimeInterval,
output: @Sendable @escaping (Success) async -> Void
) -> some Regulator<Success> {
Debouncer(dueTime: dueTime, output: output)
}
}
/// Executes an output only after a specified time interval elapses between events
///
/// ```swift
/// let debouncer = Debouncer<Int>(dueTime: .seconds(2), output: { print($0) })
///
/// for index in (0...99) {
/// DispatchQueue.global().asyncAfter(deadline: .now().advanced(by: .milliseconds(100 * index))) {
/// // pushes a value every 100 ms
/// debouncer.push(index)
/// }
/// }
///
/// // will only print "99" 2 seconds after the last call to `push(_:)`
/// ```
public final class Debouncer<Value>: @unchecked Sendable, ObservableObject, Regulator {
struct DueValue {
let value: Value
let dueTime: DispatchTime
}
struct StateMachine {
enum State {
case idle
case debouncing(value: DueValue, nextValue: DueValue?)
}
var state: State = .idle
mutating func newValue(_ value: DueValue) -> Bool {
switch self.state {
case .idle:
self.state = .debouncing(value: value, nextValue: nil)
return true
case .debouncing(let current, _):
self.state = .debouncing(value: current, nextValue: value)
return false
}
}
enum HasDebouncedOutput {
case continueDebouncing(DueValue)
case finishDebouncing
}
mutating func hasDebouncedCurrentValue() -> HasDebouncedOutput {
switch self.state {
case .idle:
fatalError("inconsistent state, a value was being debounced")
case .debouncing(_, nextValue: .some(let nextValue)):
state = .debouncing(value: nextValue, nextValue: nil)
return .continueDebouncing(nextValue)
case .debouncing(_, nextValue: .none):
state = .idle
return .finishDebouncing
}
}
}
public var output: (@Sendable (Value) async -> Void)?
public var dueTime: DispatchTimeInterval
private let lock: os_unfair_lock_t = UnsafeMutablePointer<os_unfair_lock_s>.allocate(capacity: 1)
private var stateMachine = StateMachine()
private var task: Task<Void, Never>?
public convenience init() {
self.init(dueTime: .never, output: nil)
}
/// A Regulator that executes the output only after a specified time interval elapses between events
/// - Parameters:
/// - dueTime: the time the Debouncer should wait before executing the output
/// - output: the block to execute once the regulation is done
public init(
dueTime: DispatchTimeInterval,
output: (@Sendable (Value) async -> Void)? = nil
) {
self.lock.initialize(to: os_unfair_lock())
self.dueTime = dueTime
self.output = output
}
public func push(_ value: Value) {
let newValue = DueValue(value: value, dueTime: DispatchTime.now().advanced(by: dueTime))
var shouldStartADebounce = false
os_unfair_lock_lock(self.lock)
shouldStartADebounce = self.stateMachine.newValue(newValue)
os_unfair_lock_unlock(self.lock)
if shouldStartADebounce {
self.task = Task { [weak self] in
guard let self = self else { return }
var timeToSleep = self.dueTime.nanoseconds
var currentValue = value
loop: while true {
try? await Task.sleep(nanoseconds: timeToSleep)
var output: StateMachine.HasDebouncedOutput
os_unfair_lock_lock(self.lock)
output = self.stateMachine.hasDebouncedCurrentValue()
os_unfair_lock_unlock(self.lock)
switch output {
case .finishDebouncing:
break loop
case .continueDebouncing(let value):
timeToSleep = DispatchTime.now().distance(to: value.dueTime).nanoseconds
currentValue = value.value
continue loop
}
}
await self.output?(currentValue)
}
}
}
public func cancel() {
self.task?.cancel()
}
deinit {
self.cancel()
}
}