Defaults/Sources/Defaults/Observation.swift

304 lines
9.0 KiB
Swift

import Foundation
/// TODO: Nest this inside `Defaults` if Swift ever supported nested protocols.
public protocol DefaultsObservation: AnyObject {
func invalidate()
/**
Keep this observation alive for as long as, and no longer than, another object exists.
```
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 {
private static func deserialize<Value: Decodable>(_ value: Any?, to type: Value.Type) -> Value? {
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? Value {
return value
}
// Using the array trick as done below in `UserDefaults#_set()`
return [Value].init(jsonString: "\([value])")?.first
}
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
private static func deserialize<Value: NSSecureCoding>(_ value: Any?, to type: Value.Type) -> Value? {
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? Value {
return value
}
guard let dataValue = value as? Data else {
return nil
}
return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dataValue) as? Value
}
final class BaseChange {
let kind: NSKeyValueChange
let indexes: IndexSet?
let isPrior: Bool
let newValue: Any?
let oldValue: Any?
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<Value: Codable> {
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
}
}
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public struct NSSecureCodingKeyChange<Value: NSSecureCoding> {
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
}
}
public struct OptionalKeyChange<Value: Codable> {
public let kind: NSKeyValueChange
public let indexes: IndexSet?
public let isPrior: Bool
public let newValue: Value?
public let oldValue: Value?
init(change: BaseChange) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
self.oldValue = deserialize(change.oldValue, to: Value.self)
self.newValue = deserialize(change.newValue, to: Value.self)
}
}
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public struct NSSecureCodingOptionalKeyChange<Value: NSSecureCoding> {
public let kind: NSKeyValueChange
public let indexes: IndexSet?
public let isPrior: Bool
public let newValue: Value?
public let oldValue: Value?
init(change: BaseChange) {
self.kind = change.kind
self.indexes = change.indexes
self.isPrior = change.isPrior
self.oldValue = deserialize(change.oldValue, to: Value.self)
self.newValue = deserialize(change.newValue, to: Value.self)
}
}
final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
typealias Callback = (BaseChange) -> Void
private weak var object: UserDefaults?
private let key: String
private let callback: Callback
init(object: UserDefaults, key: String, callback: @escaping Callback) {
self.object = object
self.key = key
self.callback = callback
}
deinit {
invalidate()
}
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
lifetimeAssociation?.cancel()
}
private var lifetimeAssociation: LifetimeAssociation? = nil
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
self?.invalidate()
})
return self
}
public 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 = 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 static func observe<Value: Codable>(
_ key: Defaults.Key<Value>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (KeyChange<Value>) -> Void
) -> DefaultsObservation {
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
handler(
KeyChange<Value>(change: change, defaultValue: key.defaultValue)
)
}
observation.start(options: options)
return observation
}
/**
Observe a defaults key.
*/
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public static func observe<Value: NSSecureCoding>(
_ key: Defaults.NSSecureCodingKey<Value>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (NSSecureCodingKeyChange<Value>) -> Void
) -> DefaultsObservation {
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
handler(
NSSecureCodingKeyChange<Value>(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 static func observe<Value: Codable>(
_ key: Defaults.OptionalKey<Value>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (OptionalKeyChange<Value>) -> Void
) -> DefaultsObservation {
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
handler(
OptionalKeyChange<Value>(change: change)
)
}
observation.start(options: options)
return observation
}
/**
Observe an optional defaults key.
*/
@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *)
public static func observe<Value: NSSecureCoding>(
_ key: Defaults.NSSecureCodingOptionalKey<Value>,
options: NSKeyValueObservingOptions = [.initial, .old, .new],
handler: @escaping (NSSecureCodingOptionalKeyChange<Value>) -> Void
) -> DefaultsObservation {
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
handler(
NSSecureCodingOptionalKeyChange<Value>(change: change)
)
}
observation.start(options: options)
return observation
}
}