175 lines
4.5 KiB
Swift
175 lines
4.5 KiB
Swift
|
import Foundation
|
||
|
|
||
|
/// TODO: Nest this inside `Defaults` if Swift ever supported nested protocols.
|
||
|
public protocol DefaultsObservation {
|
||
|
func invalidate()
|
||
|
}
|
||
|
|
||
|
extension Defaults {
|
||
|
private static func deserialize<T: Decodable>(_ value: Any?, to type: T.Type) -> T? {
|
||
|
guard
|
||
|
let value = value,
|
||
|
!(value is NSNull)
|
||
|
else {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// This handles the case where the value was a plist value using `isNativelySupportedType`
|
||
|
if let value = value as? T {
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
// Using the array trick as done below in `UserDefaults#_set()`
|
||
|
return [T].init(jsonString: "\([value])")?.first
|
||
|
}
|
||
|
|
||
|
fileprivate final class BaseChange {
|
||
|
fileprivate let kind: NSKeyValueChange
|
||
|
fileprivate let indexes: IndexSet?
|
||
|
fileprivate let isPrior: Bool
|
||
|
fileprivate let newValue: Any?
|
||
|
fileprivate let oldValue: Any?
|
||
|
|
||
|
fileprivate init(change: [NSKeyValueChangeKey: Any]) {
|
||
|
kind = NSKeyValueChange(rawValue: change[.kindKey] as! UInt)!
|
||
|
indexes = change[.indexesKey] as? IndexSet
|
||
|
isPrior = change[.notificationIsPriorKey] as? Bool ?? false
|
||
|
oldValue = change[.oldKey]
|
||
|
newValue = change[.newKey]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct KeyChange<T: Codable> {
|
||
|
public let kind: NSKeyValueChange
|
||
|
public let indexes: IndexSet?
|
||
|
public let isPrior: Bool
|
||
|
public let newValue: T
|
||
|
public let oldValue: T
|
||
|
|
||
|
fileprivate init(change: BaseChange, defaultValue: T) {
|
||
|
self.kind = change.kind
|
||
|
self.indexes = change.indexes
|
||
|
self.isPrior = change.isPrior
|
||
|
self.oldValue = deserialize(change.oldValue, to: T.self) ?? defaultValue
|
||
|
self.newValue = deserialize(change.newValue, to: T.self) ?? defaultValue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public struct OptionalKeyChange<T: Codable> {
|
||
|
public let kind: NSKeyValueChange
|
||
|
public let indexes: IndexSet?
|
||
|
public let isPrior: Bool
|
||
|
public let newValue: T?
|
||
|
public let oldValue: T?
|
||
|
|
||
|
fileprivate init(change: BaseChange) {
|
||
|
self.kind = change.kind
|
||
|
self.indexes = change.indexes
|
||
|
self.isPrior = change.isPrior
|
||
|
self.oldValue = deserialize(change.oldValue, to: T.self)
|
||
|
self.newValue = deserialize(change.newValue, to: T.self)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
|
||
|
fileprivate typealias Callback = (BaseChange) -> Void
|
||
|
|
||
|
private weak var object: UserDefaults?
|
||
|
private let key: String
|
||
|
private let callback: Callback
|
||
|
|
||
|
fileprivate init(object: UserDefaults, key: String, callback: @escaping Callback) {
|
||
|
self.object = object
|
||
|
self.key = key
|
||
|
self.callback = callback
|
||
|
}
|
||
|
|
||
|
deinit {
|
||
|
invalidate()
|
||
|
}
|
||
|
|
||
|
fileprivate func start(options: NSKeyValueObservingOptions) {
|
||
|
object?.addObserver(self, forKeyPath: key, options: options, context: nil)
|
||
|
}
|
||
|
|
||
|
public func invalidate() {
|
||
|
object?.removeObserver(self, forKeyPath: key, context: nil)
|
||
|
object = nil
|
||
|
}
|
||
|
|
||
|
// 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,
|
||
|
selfObject == object as? NSObject,
|
||
|
let change = change
|
||
|
else {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
callback(BaseChange(change: change))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Observe a defaults key
|
||
|
|
||
|
```
|
||
|
extension Defaults.Keys {
|
||
|
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
||
|
}
|
||
|
|
||
|
let observer = defaults.observe(.isUnicornMode) { change in
|
||
|
print(change.newValue)
|
||
|
//=> false
|
||
|
}
|
||
|
```
|
||
|
*/
|
||
|
public func observe<T: Codable>(
|
||
|
_ key: Defaults.Key<T>,
|
||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||
|
handler: @escaping (KeyChange<T>) -> Void
|
||
|
) -> DefaultsObservation {
|
||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||
|
handler(
|
||
|
KeyChange<T>(change: change, defaultValue: key.defaultValue)
|
||
|
)
|
||
|
}
|
||
|
observation.start(options: options)
|
||
|
return observation
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Observe an optional defaults key
|
||
|
|
||
|
```
|
||
|
extension Defaults.Keys {
|
||
|
static let isUnicornMode = OptionalKey<Bool>("isUnicornMode")
|
||
|
}
|
||
|
|
||
|
let observer = defaults.observe(.isUnicornMode) { change in
|
||
|
print(change.newValue)
|
||
|
//=> Optional(nil)
|
||
|
}
|
||
|
```
|
||
|
*/
|
||
|
public func observe<T: Codable>(
|
||
|
_ key: Defaults.OptionalKey<T>,
|
||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||
|
handler: @escaping (OptionalKeyChange<T>) -> Void
|
||
|
) -> DefaultsObservation {
|
||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||
|
handler(
|
||
|
OptionalKeyChange<T>(change: change)
|
||
|
)
|
||
|
}
|
||
|
observation.start(options: options)
|
||
|
return observation
|
||
|
}
|
||
|
}
|