From 1064186b3d1f225543aa8450927c20e3ee34ea16 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 24 Apr 2024 02:22:25 +0700 Subject: [PATCH] Improve performance of `Defaults.updates()` --- Sources/Defaults/Defaults.swift | 4 +- Sources/Defaults/Observation.swift | 65 +++++++++++++++++++++++++++++- Sources/Defaults/Utilities.swift | 4 +- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index a63693d..1cb2ee7 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 = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in + let observation = UserDefaultsKeyObservation2(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 = UserDefaultsKeyObservation(object: key.suite, key: key.name) { _ in + let observation = UserDefaultsKeyObservation2(object: key.suite, key: key.name) { _ in continuation.yield() } diff --git a/Sources/Defaults/Observation.swift b/Sources/Defaults/Observation.swift index d9348f7..683ef59 100644 --- a/Sources/Defaults/Observation.swift +++ b/Sources/Defaults/Observation.swift @@ -180,7 +180,7 @@ extension Defaults { } guard - selfObject == object as? NSObject, + selfObject == (object as? NSObject), let change else { return @@ -191,6 +191,69 @@ extension Defaults { 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)) } } diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index a20835b..63844db 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -1,10 +1,8 @@ import Foundation import Combine -#if DEBUG -#if canImport(OSLog) +#if DEBUG && canImport(OSLog) import OSLog #endif -#endif extension String {