304 lines
9.0 KiB
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
|
|
}
|
|
}
|