diff --git a/Sources/Defaults/Defaults+iCloud.swift b/Sources/Defaults/Defaults+iCloud.swift index 2f24c27..7f3b170 100644 --- a/Sources/Defaults/Defaults+iCloud.swift +++ b/Sources/Defaults/Defaults+iCloud.swift @@ -249,7 +249,7 @@ final class iCloudSynchronizer { @Atomic(value: []) private var remoteSyncingKeys: Set // TODO: Replace it with async stream when Swift supports custom executors. - private lazy var localKeysMonitor: Defaults.CompositeUserDefaultsAnyKeyObservation = .init { [weak self] observable in + private lazy var localKeysMonitor: Defaults.UserDefaultsAnyKeysObservation = .init { [weak self] (observable, _) in guard let self, let suite = observable.suite, diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index dfa4731..c598b8d 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -239,7 +239,7 @@ extension Defaults { initial: Bool = true ) -> AsyncStream { // TODO: Make this `some AsyncSequence` when Swift 6 is out. .init { continuation in - let observation = UserDefaultsKeyObservation2(object: key.suite, key: key.name) { change in + let observation = UserDefaultsAnyKeysObservation(object: key.suite, key: key.name) { (_, change) in // TODO: Use the `.deserialize` method directly. let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue continuation.yield(value) @@ -275,7 +275,7 @@ extension Defaults { ) -> AsyncStream { // TODO: Make this `some AsyncSequence` when Swift 6 is out. .init { continuation in let observations = keys.indexed().map { index, key in - let observation = UserDefaultsKeyObservation2(object: key.suite, key: key.name) { _ in + let observation = UserDefaultsAnyKeysObservation(object: key.suite, key: key.name) { (_, _) in continuation.yield() } diff --git a/Sources/Defaults/Observation+Combine.swift b/Sources/Defaults/Observation+Combine.swift index 18a99c5..24e4ff6 100644 --- a/Sources/Defaults/Observation+Combine.swift +++ b/Sources/Defaults/Observation+Combine.swift @@ -7,16 +7,16 @@ extension Defaults { */ final class DefaultsSubscription: Subscription where SubscriberType.Input == BaseChange { private var subscriber: SubscriberType? - private var observation: UserDefaultsKeyObservation? + private var observation: UserDefaultsAnyKeysObservation? private let options: ObservationOptions init(subscriber: SubscriberType, suite: UserDefaults, key: String, options: ObservationOptions) { self.subscriber = subscriber self.options = options - self.observation = UserDefaultsKeyObservation( + self.observation = UserDefaultsAnyKeysObservation( object: suite, key: key, - callback: observationCallback(_:) + observationCallback ) } @@ -33,7 +33,7 @@ extension Defaults { observation?.start(options: options) } - private func observationCallback(_ change: BaseChange) { + private func observationCallback(_: SuiteKeyPair, change: BaseChange) { _ = subscriber?.receive(change) } } diff --git a/Sources/Defaults/Observation.swift b/Sources/Defaults/Observation.swift index 0d9a98d..c2a950d 100644 --- a/Sources/Defaults/Observation.swift +++ b/Sources/Defaults/Observation.swift @@ -119,152 +119,15 @@ extension Defaults { 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)) - } - } - - // Same as the above, but without the lifetime utilities, which slows down invalidation and we don't need them for `.updates()`. - final class UserDefaultsKeyObservation2: NSObject { - 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 - } - - // 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)) - } - } - final class SuiteKeyPair: Hashable { weak var suite: UserDefaults? let key: String + var isObserving: Bool init(suite: UserDefaults, key: String) { self.suite = suite self.key = key + self.isObserving = false } func hash(into hasher: inout Hasher) { @@ -276,99 +139,56 @@ extension Defaults { lhs.key == rhs.key && lhs.suite == rhs.suite } - } - - private final class CompositeUserDefaultsKeyObservation: NSObject, Observation { - private static var observationContext = 0 - 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 addObserver(_ observer: NSObject, context: UnsafeMutableRawPointer?, options: ObservationOptions) { + guard !isObserving else { + return + } + suite?.addObserver(observer, forKeyPath: self.key, options: options.toNSKeyValueObservingOptions, context: context) + isObserving = true } - func start(options: ObservationOptions) { - for observable in observables { - observable.suite?.addObserver( - self, - forKeyPath: observable.key, - options: options.toNSKeyValueObservingOptions, - context: &Self.observationContext - ) + func removeObserver(_ observer: NSObject, context: UnsafeMutableRawPointer?) { + guard isObserving else { + return } + suite?.removeObserver(observer, forKeyPath: self.key, context: context) + isObserving = false } + } - func invalidate() { - for observable in observables { - observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext) - observable.suite = nil - } + final class UserDefaultsAnyKeysObservation: NSObject, Observation { + typealias Callback = (SuiteKeyPair, BaseChange) -> Void - lifetimeAssociation?.cancel() - } + public static var observationContext = 0 - func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self { - // swiftlint:disable:next trailing_closure - lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in - self?.invalidate() - }) + private var observables: Set + private var lifetimeAssociation: LifetimeAssociation? + private let callback: Callback - return self + init(object: UserDefaults, key: String, _ callback: @escaping Callback) { + self.observables = Set([SuiteKeyPair(suite: object, key: key)]) + self.callback = callback } - func removeLifetimeTie() { - lifetimeAssociation?.cancel() + init(_ keys: [Defaults._AnyKey] = [], _ callback: @escaping Callback) { + let pairs = keys.map { SuiteKeyPair(suite: $0.suite, key: $0.name) } + self.observables = Set(pairs) + self.callback = callback } - // 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)) + convenience init(_ key: Defaults._AnyKey, _ callback: @escaping Callback) { + self.init([key], callback) } - } - final class CompositeUserDefaultsAnyKeyObservation: NSObject, Observation { - typealias Callback = (SuiteKeyPair) -> Void - private static var observationContext = 1 - - private var observables: Set = [] - private var lifetimeAssociation: LifetimeAssociation? - private let callback: CompositeUserDefaultsAnyKeyObservation.Callback + deinit { + invalidate() + } - init(_ callback: @escaping CompositeUserDefaultsAnyKeyObservation.Callback) { - self.callback = callback + func start(options: ObservationOptions) { + for observable in observables { + observable.addObserver(self, context: &Self.observationContext, options: options) + } } func addObserver(_ key: Defaults._AnyKey, options: ObservationOptions = []) { @@ -378,7 +198,7 @@ extension Defaults { return } - observable.suite?.addObserver(self, forKeyPath: observable.key, options: options.toNSKeyValueObservingOptions, context: &Self.observationContext) + observable.addObserver(self, context: &Self.observationContext, options: options) } func removeObserver(_ key: Defaults._AnyKey) { @@ -387,7 +207,7 @@ extension Defaults { return } - observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext) + observable.removeObserver(self, context: &Self.observationContext) } @discardableResult @@ -406,7 +226,7 @@ extension Defaults { func invalidate() { for observable in observables { - observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext) + observable.removeObserver(self, context: &Self.observationContext) observable.suite = nil } @@ -430,13 +250,20 @@ extension Defaults { guard let object = object as? UserDefaults, + let change, let keyPath, let observable = observables.first(where: { $0.key == keyPath && $0.suite == object }) else { return } - callback(observable) + let key = preventPropagationThreadDictionaryKey + let updatingValuesFlag = (Thread.current.threadDictionary[key] as? Bool) ?? false + if updatingValuesFlag { + return + } + + callback(observable, BaseChange(change: change)) } } @@ -461,7 +288,7 @@ extension Defaults { options: ObservationOptions = [.initial], handler: @escaping (KeyChange) -> Void ) -> some Observation { - let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in + let observation = UserDefaultsAnyKeysObservation(key) { (_, change) in handler( KeyChange(change: change, defaultValue: key.defaultValue) ) @@ -491,10 +318,7 @@ extension Defaults { options: ObservationOptions = [.initial], handler: @escaping () -> Void ) -> some Observation { - let pairs = keys.map { - (suite: $0.suite, key: $0.name) - } - let compositeObservation = CompositeUserDefaultsKeyObservation(observables: pairs) { _ in + let compositeObservation = UserDefaultsAnyKeysObservation(keys) { _, _ in handler() } compositeObservation.start(options: options)