Skip to content

Commit

Permalink
Improve performance of Defaults.updates()
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Apr 23, 2024
1 parent 23e0a44 commit 1064186
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 6 deletions.
4 changes: 2 additions & 2 deletions Sources/Defaults/Defaults.swift
Expand Up @@ -239,7 +239,7 @@ extension Defaults {
initial: Bool = true
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` 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)
Expand Down Expand Up @@ -275,7 +275,7 @@ extension Defaults {
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` 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()
}

Expand Down
65 changes: 64 additions & 1 deletion Sources/Defaults/Observation.swift
Expand Up @@ -180,7 +180,7 @@ extension Defaults {
}

guard
selfObject == object as? NSObject,
selfObject == (object as? NSObject),
let change
else {
return
Expand All @@ -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))
}
}
Expand Down
4 changes: 1 addition & 3 deletions 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 {
Expand Down

0 comments on commit 1064186

Please sign in to comment.