From 573fd999a9cbc02e0e6ac53ecaecdd8bb4165d01 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Fri, 11 Apr 2025 00:10:33 +0200 Subject: [PATCH 1/8] Add customDefaults parameter to KeyboardShortcuts --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 105c465a..249977ef 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -37,6 +37,24 @@ public enum KeyboardShortcuts { private static var openMenuObserver: NSObjectProtocol? private static var closeMenuObserver: NSObjectProtocol? + + public static var customDefaults: UserDefaults? { + get { + _customDefaults + } + + set { + _customDefaults = newValue + } + } + + static var _customDefaults: UserDefaults? { + didSet { + userDefaults = _customDefaults ?? .standard + } + } + + static var userDefaults: UserDefaults = .standard /** When `true`, event handlers will not be called for registered keyboard shortcuts. @@ -59,7 +77,7 @@ public enum KeyboardShortcuts { } static var allNames: Set { - UserDefaults.standard.dictionaryRepresentation() + KeyboardShortcuts.userDefaults.dictionaryRepresentation() .compactMap { key, _ in guard key.hasPrefix(userDefaultsPrefix) else { return nil @@ -326,7 +344,7 @@ public enum KeyboardShortcuts { */ public static func getShortcut(for name: Name) -> Shortcut? { guard - let data = UserDefaults.standard.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8), + let data = KeyboardShortcuts.userDefaults.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8), let decoded = try? JSONDecoder().decode(Shortcut.self, from: data) else { return nil @@ -459,7 +477,7 @@ public enum KeyboardShortcuts { } register(shortcut) - UserDefaults.standard.set(encoded, forKey: userDefaultsKey(for: name)) + KeyboardShortcuts.userDefaults.set(encoded, forKey: userDefaultsKey(for: name)) userDefaultsDidChange(name: name) } @@ -468,7 +486,7 @@ public enum KeyboardShortcuts { return } - UserDefaults.standard.set(false, forKey: userDefaultsKey(for: name)) + KeyboardShortcuts.userDefaults.set(false, forKey: userDefaultsKey(for: name)) unregister(shortcut) userDefaultsDidChange(name: name) } @@ -478,13 +496,13 @@ public enum KeyboardShortcuts { return } - UserDefaults.standard.removeObject(forKey: userDefaultsKey(for: name)) + KeyboardShortcuts.userDefaults.removeObject(forKey: userDefaultsKey(for: name)) unregister(shortcut) userDefaultsDidChange(name: name) } static func userDefaultsContains(name: Name) -> Bool { - UserDefaults.standard.object(forKey: userDefaultsKey(for: name)) != nil + KeyboardShortcuts.userDefaults.object(forKey: userDefaultsKey(for: name)) != nil } } From ad34cf60e3419de5358ce723feda1fec58256965 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Sat, 12 Apr 2025 22:50:52 +0200 Subject: [PATCH 2/8] Observe UserDefaults changes (when coming from other instances) --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 32 +++++- Sources/KeyboardShortcuts/Utilities.swift | 101 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 249977ef..7b791795 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -37,6 +37,7 @@ public enum KeyboardShortcuts { private static var openMenuObserver: NSObjectProtocol? private static var closeMenuObserver: NSObjectProtocol? + private static var userDefaultsObservers = [UserDefaultsObservation]() public static var customDefaults: UserDefaults? { get { @@ -428,6 +429,7 @@ public enum KeyboardShortcuts { */ public static func onKeyDown(for name: Name, action: @escaping () -> Void) { legacyKeyDownHandlers[name, default: []].append(action) + startObservingShortcut(for: name) registerShortcutIfNeeded(for: name) } @@ -454,14 +456,40 @@ public enum KeyboardShortcuts { */ public static func onKeyUp(for name: Name, action: @escaping () -> Void) { legacyKeyUpHandlers[name, default: []].append(action) + startObservingShortcut(for: name) registerShortcutIfNeeded(for: name) } private static let userDefaultsPrefix = "KeyboardShortcuts_" - private static func userDefaultsKey(for shortcutName: Name) -> String { "\(userDefaultsPrefix)\(shortcutName.rawValue)" + private static func userDefaultsKey(for shortcutName: Name) -> String { + "\(userDefaultsPrefix)\(shortcutName.rawValue)" } - + + /** + Start observing UserDefaults changes for a specific shortcut name. + Only starts observation if the shortcut is not already being observed. + */ + private static func startObservingShortcut(for name: Name) { + let key = userDefaultsKey(for: name) + + let observation = UserDefaultsObservation( + suite: userDefaults, + name: name, + key: key + ) { name, value in + if value == nil { + self.unregisterShortcutIfNeeded(for: name) + } else { + self.registerShortcutIfNeeded(for: name) + } + } + + observation.start() + + userDefaultsObservers.append(observation) + } + static func userDefaultsDidChange(name: Name) { // TODO: Use proper UserDefaults observation instead of this. NotificationCenter.default.post(name: .shortcutByNameDidChange, object: nil, userInfo: ["name": name]) diff --git a/Sources/KeyboardShortcuts/Utilities.swift b/Sources/KeyboardShortcuts/Utilities.swift index bda45c87..246d463c 100644 --- a/Sources/KeyboardShortcuts/Utilities.swift +++ b/Sources/KeyboardShortcuts/Utilities.swift @@ -559,3 +559,104 @@ extension Character { self = Character(content) } } + +final class UserDefaultsObservation: NSObject { + typealias Callback = (_ name: KeyboardShortcuts.Name, _ newKeyValue: String?) -> Void + + private let name: KeyboardShortcuts.Name + private let key: String + static var observationContext = 0 + private weak var suite: UserDefaults? + private var isObserving = false + private let callback: Callback + private var lock = NSLock() + + init( + suite: UserDefaults, + name: KeyboardShortcuts.Name, + key: String, + _ callback: @escaping Callback + ) { + self.suite = suite + self.name = name + self.key = key + self.callback = callback + } + + deinit { + invalidate() + } + + func start() { + lock.lock() + + guard !isObserving else { + return + } + + suite?.addObserver( + self, + forKeyPath: key, + options: [.new], + context: &Self.observationContext + ) + isObserving = true + + lock.unlock() + } + + func invalidate() { + lock.lock() + + guard isObserving else { + return + } + + suite?.removeObserver( + self, + forKeyPath: key + ) + isObserving = false + suite = nil + + lock.unlock() + } + + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer? + ) { + guard + context == &Self.observationContext + else { + super.observeValue( + forKeyPath: keyPath, + of: object, + change: change, + context: context + ) + return + } + + guard let selfSuite = suite else { + invalidate() + return + } + + guard + selfSuite == (object as? UserDefaults), + let change + else { + return + } + + guard keyPath == key else { + return + } + + let encodedString = change[.newKey] as? String + callback(self.name, encodedString) + } +} From 66ce866319a4e3f9b4f3fa7a0aaf38c8cb0a5090 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Tue, 29 Apr 2025 22:17:26 +0200 Subject: [PATCH 3/8] Simplify and add documentation to userDefaults --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 7b791795..15db9ec6 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -39,23 +39,24 @@ public enum KeyboardShortcuts { private static var closeMenuObserver: NSObjectProtocol? private static var userDefaultsObservers = [UserDefaultsObservation]() - public static var customDefaults: UserDefaults? { - get { - _customDefaults - } - - set { - _customDefaults = newValue - } - } - - static var _customDefaults: UserDefaults? { - didSet { - userDefaults = _customDefaults ?? .standard - } - } - - static var userDefaults: UserDefaults = .standard + /** + The UserDefaults instance used to store and retrieve keyboard shortcut configurations. + + By default, this uses the standard UserDefaults instance. You can customize this to use a different UserDefaults instance, + for example, to store shortcuts in a specific app group or to use a custom UserDefaults instance for testing. + + ```swift + // Example: Using a custom UserDefaults instance + KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "com.example.appgroup")! + + // Example: Using a custom UserDefaults instance for testing + KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "test")! + ``` + + - Important: Changing this property will not migrate existing shortcuts from the previous UserDefaults instance. + - Note: All keyboard shortcut configurations are stored with the prefix "KeyboardShortcuts_" to avoid conflicts with other app data. + */ + public static var userDefaults: UserDefaults = .standard /** When `true`, event handlers will not be called for registered keyboard shortcuts. From cd80964db78e59826dc8db475466f97a89d24646 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 30 Apr 2025 16:00:54 +0700 Subject: [PATCH 4/8] Update KeyboardShortcuts.swift --- Sources/KeyboardShortcuts/KeyboardShortcuts.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 15db9ec6..b06bef99 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -40,23 +40,22 @@ public enum KeyboardShortcuts { private static var userDefaultsObservers = [UserDefaultsObservation]() /** - The UserDefaults instance used to store and retrieve keyboard shortcut configurations. + The `UserDefaults` instance used to store and retrieve keyboard shortcut configurations. - By default, this uses the standard UserDefaults instance. You can customize this to use a different UserDefaults instance, - for example, to store shortcuts in a specific app group or to use a custom UserDefaults instance for testing. + By default, this uses the standard `UserDefaults` instance. You can customize this to use a different `UserDefaults` instance, for example, to store shortcuts in a specific app group or to use a custom `UserDefaults` instance for testing. ```swift // Example: Using a custom UserDefaults instance - KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "com.example.appgroup")! + KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "com.example.suite")! // Example: Using a custom UserDefaults instance for testing KeyboardShortcuts.userDefaults = UserDefaults(suiteName: "test")! ``` - - Important: Changing this property will not migrate existing shortcuts from the previous UserDefaults instance. - - Note: All keyboard shortcut configurations are stored with the prefix "KeyboardShortcuts_" to avoid conflicts with other app data. + - Important: Changing this property will not migrate existing shortcuts from the previous `UserDefaults` instance. + - Note: All keyboard shortcut configurations are stored with the prefix `KeyboardShortcuts_` to avoid conflicts with other app data. */ - public static var userDefaults: UserDefaults = .standard + public static var userDefaults = UserDefaults.standard /** When `true`, event handlers will not be called for registered keyboard shortcuts. From 6ac67e737b42d301bbd76dfb75f8cac5a8017501 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Sat, 3 May 2025 00:44:51 +0200 Subject: [PATCH 5/8] Fix swiftlint warnings --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 26 +++++++------- Sources/KeyboardShortcuts/Utilities.swift | 36 ++++++++++--------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index b06bef99..28eae93b 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -38,7 +38,7 @@ public enum KeyboardShortcuts { private static var openMenuObserver: NSObjectProtocol? private static var closeMenuObserver: NSObjectProtocol? private static var userDefaultsObservers = [UserDefaultsObservation]() - + /** The `UserDefaults` instance used to store and retrieve keyboard shortcut configurations. @@ -78,7 +78,7 @@ public enum KeyboardShortcuts { } static var allNames: Set { - KeyboardShortcuts.userDefaults.dictionaryRepresentation() + Self.userDefaults.dictionaryRepresentation() .compactMap { key, _ in guard key.hasPrefix(userDefaultsPrefix) else { return nil @@ -345,7 +345,7 @@ public enum KeyboardShortcuts { */ public static func getShortcut(for name: Name) -> Shortcut? { guard - let data = KeyboardShortcuts.userDefaults.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8), + let data = Self.userDefaults.string(forKey: userDefaultsKey(for: name))?.data(using: .utf8), let decoded = try? JSONDecoder().decode(Shortcut.self, from: data) else { return nil @@ -462,17 +462,17 @@ public enum KeyboardShortcuts { private static let userDefaultsPrefix = "KeyboardShortcuts_" - private static func userDefaultsKey(for shortcutName: Name) -> String { + private static func userDefaultsKey(for shortcutName: Name) -> String { "\(userDefaultsPrefix)\(shortcutName.rawValue)" } - + /** Start observing UserDefaults changes for a specific shortcut name. Only starts observation if the shortcut is not already being observed. */ private static func startObservingShortcut(for name: Name) { let key = userDefaultsKey(for: name) - + let observation = UserDefaultsObservation( suite: userDefaults, name: name, @@ -484,12 +484,12 @@ public enum KeyboardShortcuts { self.registerShortcutIfNeeded(for: name) } } - + observation.start() - + userDefaultsObservers.append(observation) } - + static func userDefaultsDidChange(name: Name) { // TODO: Use proper UserDefaults observation instead of this. NotificationCenter.default.post(name: .shortcutByNameDidChange, object: nil, userInfo: ["name": name]) @@ -505,7 +505,7 @@ public enum KeyboardShortcuts { } register(shortcut) - KeyboardShortcuts.userDefaults.set(encoded, forKey: userDefaultsKey(for: name)) + Self.userDefaults.set(encoded, forKey: userDefaultsKey(for: name)) userDefaultsDidChange(name: name) } @@ -514,7 +514,7 @@ public enum KeyboardShortcuts { return } - KeyboardShortcuts.userDefaults.set(false, forKey: userDefaultsKey(for: name)) + Self.userDefaults.set(false, forKey: userDefaultsKey(for: name)) unregister(shortcut) userDefaultsDidChange(name: name) } @@ -524,13 +524,13 @@ public enum KeyboardShortcuts { return } - KeyboardShortcuts.userDefaults.removeObject(forKey: userDefaultsKey(for: name)) + Self.userDefaults.removeObject(forKey: userDefaultsKey(for: name)) unregister(shortcut) userDefaultsDidChange(name: name) } static func userDefaultsContains(name: Name) -> Bool { - KeyboardShortcuts.userDefaults.object(forKey: userDefaultsKey(for: name)) != nil + Self.userDefaults.object(forKey: userDefaultsKey(for: name)) != nil } } diff --git a/Sources/KeyboardShortcuts/Utilities.swift b/Sources/KeyboardShortcuts/Utilities.swift index 246d463c..4b9023ba 100644 --- a/Sources/KeyboardShortcuts/Utilities.swift +++ b/Sources/KeyboardShortcuts/Utilities.swift @@ -562,7 +562,7 @@ extension Character { final class UserDefaultsObservation: NSObject { typealias Callback = (_ name: KeyboardShortcuts.Name, _ newKeyValue: String?) -> Void - + private let name: KeyboardShortcuts.Name private let key: String static var observationContext = 0 @@ -570,7 +570,9 @@ final class UserDefaultsObservation: NSObject { private var isObserving = false private let callback: Callback private var lock = NSLock() - + + private var token: NSKeyValueObservation? = nil + init( suite: UserDefaults, name: KeyboardShortcuts.Name, @@ -582,18 +584,18 @@ final class UserDefaultsObservation: NSObject { self.key = key self.callback = callback } - + deinit { invalidate() } - + func start() { lock.lock() - + guard !isObserving else { return } - + suite?.addObserver( self, forKeyPath: key, @@ -601,31 +603,31 @@ final class UserDefaultsObservation: NSObject { context: &Self.observationContext ) isObserving = true - + lock.unlock() } - + func invalidate() { lock.lock() - + guard isObserving else { return } - + suite?.removeObserver( self, forKeyPath: key ) isObserving = false suite = nil - + lock.unlock() } - + override func observeValue( forKeyPath keyPath: String?, of object: Any?, - change: [NSKeyValueChangeKey : Any]?, + change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer? ) { guard @@ -639,23 +641,23 @@ final class UserDefaultsObservation: NSObject { ) return } - + guard let selfSuite = suite else { invalidate() return } - + guard selfSuite == (object as? UserDefaults), let change else { return } - + guard keyPath == key else { return } - + let encodedString = change[.newKey] as? String callback(self.name, encodedString) } From e392a8430e9ae39542de9d58be172ac225240a38 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Sat, 3 May 2025 09:45:13 +0200 Subject: [PATCH 6/8] UserDefaultsObservation: improve locking and remove KeyboardShortcuts.Name +++ Fix swiftlint warnings --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 3 +- Sources/KeyboardShortcuts/Utilities.swift | 58 ++++++++----------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 28eae93b..82770920 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -475,9 +475,8 @@ public enum KeyboardShortcuts { let observation = UserDefaultsObservation( suite: userDefaults, - name: name, key: key - ) { name, value in + ) { value in if value == nil { self.unregisterShortcutIfNeeded(for: name) } else { diff --git a/Sources/KeyboardShortcuts/Utilities.swift b/Sources/KeyboardShortcuts/Utilities.swift index 4b9023ba..6de2098e 100644 --- a/Sources/KeyboardShortcuts/Utilities.swift +++ b/Sources/KeyboardShortcuts/Utilities.swift @@ -561,9 +561,8 @@ extension Character { } final class UserDefaultsObservation: NSObject { - typealias Callback = (_ name: KeyboardShortcuts.Name, _ newKeyValue: String?) -> Void + typealias Callback = (_ newKeyValue: String?) -> Void - private let name: KeyboardShortcuts.Name private let key: String static var observationContext = 0 private weak var suite: UserDefaults? @@ -571,16 +570,12 @@ final class UserDefaultsObservation: NSObject { private let callback: Callback private var lock = NSLock() - private var token: NSKeyValueObservation? = nil - init( suite: UserDefaults, - name: KeyboardShortcuts.Name, key: String, _ callback: @escaping Callback ) { self.suite = suite - self.name = name self.key = key self.callback = callback } @@ -590,44 +585,41 @@ final class UserDefaultsObservation: NSObject { } func start() { - lock.lock() + lock.withLock { + guard !isObserving else { + return + } - guard !isObserving else { - return + suite?.addObserver( + self, + forKeyPath: key, + options: [.new], + context: &Self.observationContext + ) + isObserving = true } - - suite?.addObserver( - self, - forKeyPath: key, - options: [.new], - context: &Self.observationContext - ) - isObserving = true - - lock.unlock() } func invalidate() { - lock.lock() + lock.withLock { + guard isObserving else { + return + } - guard isObserving else { - return + suite?.removeObserver( + self, + forKeyPath: key + ) + isObserving = false + suite = nil } - - suite?.removeObserver( - self, - forKeyPath: key - ) - isObserving = false - suite = nil - - lock.unlock() } + // swiftlint:disable:next block_based_kvo override func observeValue( forKeyPath keyPath: String?, of object: Any?, - change: [NSKeyValueChangeKey: Any]?, + change: [NSKeyValueChangeKey: Any]?, // swiftlint:disable:this discouraged_optional_collection context: UnsafeMutableRawPointer? ) { guard @@ -659,6 +651,6 @@ final class UserDefaultsObservation: NSObject { } let encodedString = change[.newKey] as? String - callback(self.name, encodedString) + callback(encodedString) } } From 6b4bfc278feaa95bc57fa7f2fc568bd6e7ffcf78 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Sun, 4 May 2025 00:14:52 +0200 Subject: [PATCH 7/8] Invalidating userDefaultsObservers in removeAllHandlers +++ Restart potentially existing observer +++ Update observer if UserDefaults being changed --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 44 +++++++++++++++---- Sources/KeyboardShortcuts/Utilities.swift | 9 +++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 82770920..afb11e6e 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -55,7 +55,13 @@ public enum KeyboardShortcuts { - Important: Changing this property will not migrate existing shortcuts from the previous `UserDefaults` instance. - Note: All keyboard shortcut configurations are stored with the prefix `KeyboardShortcuts_` to avoid conflicts with other app data. */ - public static var userDefaults = UserDefaults.standard + public static var userDefaults = UserDefaults.standard { + didSet { + for observer in userDefaultsObservers { + observer.update(suite: userDefaults) + } + } + } /** When `true`, event handlers will not be called for registered keyboard shortcuts. @@ -198,6 +204,12 @@ public enum KeyboardShortcuts { legacyKeyDownHandlers = [:] legacyKeyUpHandlers = [:] + + // invalidate and remove all elements of userDefaultsObservers + for observation in userDefaultsObservers { + observation.invalidate() + } + userDefaultsObservers.removeAll() } // TODO: Also add `.isEnabled(_ name: Name)`. @@ -429,7 +441,7 @@ public enum KeyboardShortcuts { */ public static func onKeyDown(for name: Name, action: @escaping () -> Void) { legacyKeyDownHandlers[name, default: []].append(action) - startObservingShortcut(for: name) + startObservingShortcutIfNeeded(for: name) registerShortcutIfNeeded(for: name) } @@ -456,7 +468,7 @@ public enum KeyboardShortcuts { */ public static func onKeyUp(for name: Name, action: @escaping () -> Void) { legacyKeyUpHandlers[name, default: []].append(action) - startObservingShortcut(for: name) + startObservingShortcutIfNeeded(for: name) registerShortcutIfNeeded(for: name) } @@ -468,12 +480,28 @@ public enum KeyboardShortcuts { /** Start observing UserDefaults changes for a specific shortcut name. - Only starts observation if the shortcut is not already being observed. + + This function manages the lifecycle of observations for keyboard shortcuts in the given suite (e.g. UserDefaults): + - Checks if the shortcut is already being observed + - If already observed, restarts the observation + - If not observed, creates a new observation and adds it to the observers list + + The observation handles changes to the shortcut configuration in the suite: + - When the shortcut is removed (value becomes nil), unregisters the shortcut + - When the shortcut is added or modified, registers the new shortcut + + - Parameter name: The name of the shortcut to observe */ - private static func startObservingShortcut(for name: Name) { + private static func startObservingShortcutIfNeeded(for name: Name) { let key = userDefaultsKey(for: name) - let observation = UserDefaultsObservation( + // check userDefaultsObservers to see if we are already observing this key + if let observer = userDefaultsObservers.first(where: { $0.key == key }) { + observer.start() + return + } + + let observer = UserDefaultsObservation( suite: userDefaults, key: key ) { value in @@ -484,9 +512,9 @@ public enum KeyboardShortcuts { } } - observation.start() + observer.start() - userDefaultsObservers.append(observation) + userDefaultsObservers.append(observer) } static func userDefaultsDidChange(name: Name) { diff --git a/Sources/KeyboardShortcuts/Utilities.swift b/Sources/KeyboardShortcuts/Utilities.swift index 6de2098e..e268953f 100644 --- a/Sources/KeyboardShortcuts/Utilities.swift +++ b/Sources/KeyboardShortcuts/Utilities.swift @@ -563,7 +563,8 @@ extension Character { final class UserDefaultsObservation: NSObject { typealias Callback = (_ newKeyValue: String?) -> Void - private let key: String + let key: String + static var observationContext = 0 private weak var suite: UserDefaults? private var isObserving = false @@ -615,6 +616,12 @@ final class UserDefaultsObservation: NSObject { } } + func update(suite new: UserDefaults) { + invalidate() + suite = new + start() + } + // swiftlint:disable:next block_based_kvo override func observeValue( forKeyPath keyPath: String?, From 5cce327a610d59569de0977e9fdda75931ccb7e1 Mon Sep 17 00:00:00 2001 From: Robert Hahn Date: Sun, 4 May 2025 22:55:46 +0200 Subject: [PATCH 8/8] Observe changes to UserDefaults instance also for events +++ Other minor readability improvements --- .../KeyboardShortcuts/KeyboardShortcuts.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index afb11e6e..110019dc 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -206,8 +206,8 @@ public enum KeyboardShortcuts { legacyKeyUpHandlers = [:] // invalidate and remove all elements of userDefaultsObservers - for observation in userDefaultsObservers { - observation.invalidate() + for observer in userDefaultsObservers { + observer.invalidate() } userDefaultsObservers.removeAll() } @@ -441,8 +441,10 @@ public enum KeyboardShortcuts { */ public static func onKeyDown(for name: Name, action: @escaping () -> Void) { legacyKeyDownHandlers[name, default: []].append(action) - startObservingShortcutIfNeeded(for: name) registerShortcutIfNeeded(for: name) + + // observe changes to the UserDefaults instance for the given shortcut name + startObservingShortcutIfNeeded(for: name) } /** @@ -468,8 +470,10 @@ public enum KeyboardShortcuts { */ public static func onKeyUp(for name: Name, action: @escaping () -> Void) { legacyKeyUpHandlers[name, default: []].append(action) - startObservingShortcutIfNeeded(for: name) registerShortcutIfNeeded(for: name) + + // observe changes to the UserDefaults instance for the given shortcut name + startObservingShortcutIfNeeded(for: name) } private static let userDefaultsPrefix = "KeyboardShortcuts_" @@ -479,9 +483,9 @@ public enum KeyboardShortcuts { } /** - Start observing UserDefaults changes for a specific shortcut name. - - This function manages the lifecycle of observations for keyboard shortcuts in the given suite (e.g. UserDefaults): + Start observing changes to the `UserDefaults` instance for a specific shortcut name. + + This function manages the lifecycle of observations for keyboard shortcuts in the given `UserDefaults` instance (set by `userDefaults` property): - Checks if the shortcut is already being observed - If already observed, restarts the observation - If not observed, creates a new observation and adds it to the observers list @@ -610,6 +614,9 @@ extension KeyboardShortcuts { } registerShortcutIfNeeded(for: name) + + // observe changes to the UserDefaults instance for the given shortcut name + startObservingShortcutIfNeeded(for: name) } continuation.onTermination = { _ in