-
-
Notifications
You must be signed in to change notification settings - Fork 114
/
Observation.swift
368 lines (301 loc) · 9.84 KB
/
Observation.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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
import Foundation
public protocol _DefaultsObservation: AnyObject {
func invalidate()
/**
Keep this observation alive for as long as, and no longer than, another object exists.
```swift
Defaults.observe(.xyz) { [unowned self] change in
self.xyz = change.newValue
}.tieToLifetime(of: self)
```
*/
@discardableResult
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self
/**
Break the lifetime tie created by `tieToLifetime(of:)`, if one exists.
- Postcondition: The effects of any call to `tieToLifetime(of:)` are reversed.
- Note: If the tied-to object has already died, then self is considered to be invalidated, and this method has no logical effect.
*/
func removeLifetimeTie()
}
extension Defaults {
public typealias Observation = _DefaultsObservation
public enum ObservationOption: Sendable {
/**
Whether a notification should be sent to the observer immediately, before the observer registration method even returns.
*/
case initial
/**
Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change.
*/
case prior
}
public typealias ObservationOptions = Set<ObservationOption>
private static func deserialize<Value: Serializable>(_ value: Any?, to type: Value.Type) -> Value? {
guard
let value,
!(value is NSNull)
else {
return nil
}
return Value.toValue(value)
}
struct BaseChange {
let kind: NSKeyValueChange
let indexes: IndexSet?
let isPrior: Bool
let newValue: Any?
let oldValue: Any?
init(change: [NSKeyValueChangeKey: Any]) {
self.kind = NSKeyValueChange(rawValue: change[.kindKey] as! UInt)!
self.indexes = change[.indexesKey] as? IndexSet
self.isPrior = change[.notificationIsPriorKey] as? Bool ?? false
self.oldValue = change[.oldKey]
self.newValue = change[.newKey]
}
}
public struct KeyChange<Value: Serializable> {
public let kind: NSKeyValueChange
public let indexes: IndexSet?
public let isPrior: Bool
public let newValue: Value
public let oldValue: Value
init(change: BaseChange, defaultValue: Value) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
self.oldValue = deserialize(change.oldValue, to: Value.self) ?? defaultValue
self.newValue = deserialize(change.newValue, to: Value.self) ?? defaultValue
}
}
private static var preventPropagationThreadDictionaryKey: String {
"\(type(of: Observation.self))_threadUpdatingValuesFlag"
}
/**
Execute the closure without triggering change events.
Any `Defaults` key changes made within the closure will not propagate to `Defaults` event listeners (`Defaults.observe()` and `Defaults.publisher()`). This can be useful to prevent infinite recursion when you want to change a key in the callback listening to changes for the same key.
- Note: This only works with `Defaults.observe()` and `Defaults.publisher()`. User-made KVO will not be affected.
```swift
let observer = Defaults.observe(keys: .key1, .key2) {
// …
Defaults.withoutPropagation {
// Update `.key1` without propagating the change to listeners.
Defaults[.key1] = 11
}
// This will be propagated.
Defaults[.someKey] = true
}
```
*/
public static func withoutPropagation(_ closure: () -> Void) {
// How does it work?
// KVO observation callbacks are executed right after a change is made, and run on the same thread as the caller. So it works by storing a flag in the current thread's dictionary, which is then evaluated in the callback.
let key = preventPropagationThreadDictionaryKey
Thread.current.threadDictionary[key] = true
closure()
Thread.current.threadDictionary[key] = false
}
final class UserDefaultsKeyObservation: NSObject, Observation {
typealias Callback = (BaseChange) -> Void
private weak var object: UserDefaults?
private let key: String
private let callback: Callback
private var isObserving = false
init(object: UserDefaults, key: String, callback: @escaping Callback) {
self.object = object
self.key = key
self.callback = callback
}
deinit {
invalidate()
}
func start(options: ObservationOptions) {
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
isObserving = true
}
func invalidate() {
if isObserving {
object?.removeObserver(self, forKeyPath: key, context: nil)
isObserving = false
}
object = nil
lifetimeAssociation?.cancel()
}
private var lifetimeAssociation: LifetimeAssociation?
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
// swiftlint:disable:next trailing_closure
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
})
return self
}
func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}
// swiftlint:disable:next block_based_kvo
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?, // swiftlint:disable:this discouraged_optional_collection
context: UnsafeMutableRawPointer?
) {
guard let selfObject = self.object else {
invalidate()
return
}
guard
selfObject == object as? NSObject,
let change
else {
return
}
let key = preventPropagationThreadDictionaryKey
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
guard !updatingValuesFlag else {
return
}
callback(BaseChange(change: change))
}
}
private final class CompositeUserDefaultsKeyObservation: NSObject, Observation {
private static var observationContext = 0
private final class SuiteKeyPair {
weak var suite: UserDefaults?
let key: String
init(suite: UserDefaults, key: String) {
self.suite = suite
self.key = key
}
}
private var observables: [SuiteKeyPair]
private var lifetimeAssociation: LifetimeAssociation?
private let callback: UserDefaultsKeyObservation.Callback
init(observables: [(suite: UserDefaults, key: String)], callback: @escaping UserDefaultsKeyObservation.Callback) {
self.observables = observables.map { SuiteKeyPair(suite: $0.suite, key: $0.key) }
self.callback = callback
super.init()
}
deinit {
invalidate()
}
func start(options: ObservationOptions) {
for observable in observables {
observable.suite?.addObserver(
self,
forKeyPath: observable.key,
options: options.toNSKeyValueObservingOptions,
context: &Self.observationContext
)
}
}
func invalidate() {
for observable in observables {
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
observable.suite = nil
}
lifetimeAssociation?.cancel()
}
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
// swiftlint:disable:next trailing_closure
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
})
return self
}
func removeLifetimeTie() {
lifetimeAssociation?.cancel()
}
// swiftlint:disable:next block_based_kvo
override func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?, // swiftlint:disable:this discouraged_optional_collection
context: UnsafeMutableRawPointer?
) {
guard
context == &Self.observationContext
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
guard
object is UserDefaults,
let change
else {
return
}
let key = preventPropagationThreadDictionaryKey
let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false
if updatingValuesFlag {
return
}
callback(BaseChange(change: change))
}
}
/**
Observe a defaults key.
```swift
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
let observer = Defaults.observe(.isUnicornMode) { change in
print(change.newValue)
//=> false
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func observe<Value: Serializable>(
_ key: Key<Value>,
options: ObservationOptions = [.initial],
handler: @escaping (KeyChange<Value>) -> Void
) -> Observation {
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
handler(
KeyChange(change: change, defaultValue: key.defaultValue)
)
}
observation.start(options: options)
return observation
}
/**
Observe multiple keys of any type, but without any information about the changes.
```swift
extension Defaults.Keys {
static let setting1 = Key<Bool>("setting1", default: false)
static let setting2 = Key<Bool>("setting2", default: true)
}
let observer = Defaults.observe(keys: .setting1, .setting2) {
// …
}
```
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
*/
public static func observe(
keys: _AnyKey...,
options: ObservationOptions = [.initial],
handler: @escaping () -> Void
) -> Observation {
let pairs = keys.map {
(suite: $0.suite, key: $0.name)
}
let compositeObservation = CompositeUserDefaultsKeyObservation(observables: pairs) { _ in
handler()
}
compositeObservation.start(options: options)
return compositeObservation
}
}
extension Defaults.ObservationOptions {
var toNSKeyValueObservingOptions: NSKeyValueObservingOptions {
var options: NSKeyValueObservingOptions = [.old, .new]
if contains(.initial) {
options.insert(.initial)
} else if contains(.prior) {
options.insert(.prior)
}
return options
}
}
extension Defaults.KeyChange: Equatable where Value: Equatable {}
extension Defaults.KeyChange: Sendable where Value: Sendable {}