diff --git a/Sources/UserDefaultsDependency/UserDefaultsDependency.swift b/Sources/UserDefaultsDependency/UserDefaultsDependency.swift index 135d1bb..9513b7d 100644 --- a/Sources/UserDefaultsDependency/UserDefaultsDependency.swift +++ b/Sources/UserDefaultsDependency/UserDefaultsDependency.swift @@ -1,6 +1,7 @@ import Dependencies -import Foundation @_spi(Internals) import DependenciesAdditionsBasics +import Foundation + extension DependencyValues { /// A dependency that exposes an ``UserDefaults.Dependency`` value that you can use to read and /// write to `UserDefaults`. @@ -47,7 +48,8 @@ extension UserDefaults { /// - Parameter key: The key that references this user preference. /// - Returns: An `AsyncSequence` of `T?` values, including the initial value. @_spi(Internals) - public func values(forKey key: String) -> AsyncMapSequence, T?> { + public func values(forKey key: String) -> AsyncMapSequence, T?> + { self._values(key, T.self).map { $0 as? T } } } @@ -73,49 +75,49 @@ extension UserDefaults.Dependency: DependencyKey { // We use KVO to also get out-of-process changes AsyncStream((any Sendable)?.self) { continuation in #if canImport(ObjectiveC) - final class Observer: NSObject, Sendable { - let key: String - let type: Any.Type - let onChange: @Sendable ((any Sendable)?) -> Void - init( - key: String, - type: Any.Type, - onChange: @escaping @Sendable ((any Sendable)?) -> Void - ) { - self.key = key - self.type = type - self.onChange = onChange - super.init() - } + final class Observer: NSObject, Sendable { + let key: String + let type: Any.Type + let onChange: @Sendable ((any Sendable)?) -> Void + init( + key: String, + type: Any.Type, + onChange: @escaping @Sendable ((any Sendable)?) -> Void + ) { + self.key = key + self.type = type + self.onChange = onChange + super.init() + } - override func observeValue( - forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey: Any]?, - context: UnsafeMutableRawPointer? - ) { - self.onChange( - (object as! UserDefaults).getSendable(forKey: self.key, as: self.type) - ) + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer? + ) { + self.onChange( + (object as! UserDefaults).getSendable(forKey: self.key, as: self.type) + ) + } } - } - let object = Observer(key: key, type: type) { - continuation.yield($0) - } + let object = Observer(key: key, type: type) { + continuation.yield($0) + } - userDefaults.value.addObserver( - object, - forKeyPath: key, - options: [.initial, .new], - context: nil - ) - continuation.onTermination = { _ in - userDefaults.value.removeObserver(object, forKeyPath: key) - } + userDefaults.value.addObserver( + object, + forKeyPath: key, + options: [.initial, .new], + context: nil + ) + continuation.onTermination = { _ in + userDefaults.value.removeObserver(object, forKeyPath: key) + } #else - print("AsyncStream of UserDefaults values is currently not supported on Linux") - continuation.finish() + print("AsyncStream of UserDefaults values is currently not supported on Linux") + continuation.finish() #endif } } @@ -133,12 +135,12 @@ extension UserDefaults.Dependency: DependencyKey { // TODO: NSUbiquitousKeyValueStore variant? } -private extension UserDefaults { - func contains(key: String) -> Bool { +extension UserDefaults { + fileprivate func contains(key: String) -> Bool { self.object(forKey: key) != nil } - func getSendable(forKey key: String, as type: Any.Type) -> (any Sendable)? { + fileprivate func getSendable(forKey key: String, as type: Any.Type) -> (any Sendable)? { switch type { case let type where type == Bool.self, let type where type == Bool?.self: guard self.contains(key: key) else { return nil } @@ -162,7 +164,7 @@ private extension UserDefaults { } } - func setSendable(_ value: (any Sendable)?, forKey key: String) { + fileprivate func setSendable(_ value: (any Sendable)?, forKey key: String) { guard let value = value else { self.removeObject(forKey: key) return @@ -188,64 +190,64 @@ private extension UserDefaults { } } #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS) -@available(iOS 5.0, tvOS 9.0, macOS 10.7, watchOS 9.0, *) -private extension NSUbiquitousKeyValueStore { - func contains(key: String) -> Bool { - self.object(forKey: key) != nil - } - - func getSendable(forKey key: String, as type: Any.Type) -> (any Sendable)? { - switch type { - case let type where type == Bool.self, let type where type == Bool?.self: - guard self.contains(key: key) else { return nil } - return self.bool(forKey: key) - case let type where type == Data.self, let type where type == Data?.self: - return self.data(forKey: key) - case let type where type == Date.self, let type where type == Date?.self: - return self.object(forKey: key) as? Date - case let type where type == Double.self, let type where type == Double?.self: - guard self.contains(key: key) else { return nil } - return self.double(forKey: key) - case let type where type == Int.self, let type where type == Int?.self: - guard self.contains(key: key) else { return nil } - return Int(self.longLong(forKey: key)) - case let type where type == String.self, let type where type == String?.self: - return self.string(forKey: key) - case let type where type == URL.self, let type where type == URL?.self: - // TODO: Improve to handle file urls - guard let string = self.string(forKey: key) else { return nil } - return URL(string: string) - default: - return nil + @available(iOS 5.0, tvOS 9.0, macOS 10.7, watchOS 9.0, *) + extension NSUbiquitousKeyValueStore { + fileprivate func contains(key: String) -> Bool { + self.object(forKey: key) != nil } - } - func setSendable(_ value: (any Sendable)?, forKey key: String) { - guard let value = value else { - self.removeObject(forKey: key) - return + fileprivate func getSendable(forKey key: String, as type: Any.Type) -> (any Sendable)? { + switch type { + case let type where type == Bool.self, let type where type == Bool?.self: + guard self.contains(key: key) else { return nil } + return self.bool(forKey: key) + case let type where type == Data.self, let type where type == Data?.self: + return self.data(forKey: key) + case let type where type == Date.self, let type where type == Date?.self: + return self.object(forKey: key) as? Date + case let type where type == Double.self, let type where type == Double?.self: + guard self.contains(key: key) else { return nil } + return self.double(forKey: key) + case let type where type == Int.self, let type where type == Int?.self: + guard self.contains(key: key) else { return nil } + return Int(self.longLong(forKey: key)) + case let type where type == String.self, let type where type == String?.self: + return self.string(forKey: key) + case let type where type == URL.self, let type where type == URL?.self: + // TODO: Improve to handle file urls + guard let string = self.string(forKey: key) else { return nil } + return URL(string: string) + default: + return nil + } } - switch value { - case let value as Bool: - self.set(value, forKey: key) - case let value as Data: - self.set(value, forKey: key) - case let value as Date: - self.set(value, forKey: key) - case let value as Double: - self.set(value, forKey: key) - case let value as Int: - self.set(value, forKey: key) - case let value as String: - self.set(value, forKey: key) - case let value as URL: - // TODO: Improve to handle file urls - self.set(value.absoluteString, forKey: key) - default: - return + + fileprivate func setSendable(_ value: (any Sendable)?, forKey key: String) { + guard let value = value else { + self.removeObject(forKey: key) + return + } + switch value { + case let value as Bool: + self.set(value, forKey: key) + case let value as Data: + self.set(value, forKey: key) + case let value as Date: + self.set(value, forKey: key) + case let value as Double: + self.set(value, forKey: key) + case let value as Int: + self.set(value, forKey: key) + case let value as String: + self.set(value, forKey: key) + case let value as URL: + // TODO: Improve to handle file urls + self.set(value.absoluteString, forKey: key) + default: + return + } } } -} #endif extension UserDefaults.Dependency: TestDependencyKey { public static let testValue: Self = { @@ -295,7 +297,7 @@ extension UserDefaults.Dependency: TestDependencyKey { } } -fileprivate func _isEqual(_ lhs: (any Sendable)?, _ rhs: (any Sendable)?) -> Bool { +private func _isEqual(_ lhs: (any Sendable)?, _ rhs: (any Sendable)?) -> Bool { switch (lhs, rhs) { case let (.some(lhs), .some(rhs)): return (lhs as! any Equatable).isEqual(other: rhs) @@ -312,73 +314,74 @@ extension Equatable { } #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS) -extension UserDefaults.Dependency { - /// An iCloud-based container of key-value pairs you use to share data among - /// instances of your app running on a user's connected devices. - @available(iOS 5.0, tvOS 9.0, macOS 10.7, watchOS 9.0, *) - public static var ubiquitous: UserDefaults.Dependency { - let store = NSUbiquitousKeyValueStore.default - let userDefaults = UncheckedSendable(store) - let subject = AsyncSharedSubject<(String, any Sendable)>() + extension UserDefaults.Dependency { + /// An iCloud-based container of key-value pairs you use to share data among + /// instances of your app running on a user's connected devices. + @available(iOS 5.0, tvOS 9.0, macOS 10.7, watchOS 9.0, *) + public static var ubiquitous: UserDefaults.Dependency { + let store = NSUbiquitousKeyValueStore.default + let userDefaults = UncheckedSendable(store) + let subject = AsyncSharedSubject<(String, any Sendable)>() - return UserDefaults.Dependency { key, type in - userDefaults.value.getSendable(forKey: key, as: type) - } set: { - userDefaults.value.setSendable($0, forKey: $1) - // NSUbiquitousKeyValueStore doesn't support KVO, so we call directly - // the continuation for local changes. - subject.yield(($1, $0)) - userDefaults.value.synchronize() - } values: { key, type in - AsyncStream((any Sendable)?.self) { continuation in - final class Observer: NSObject, Sendable { - let key: String - let type: Any.Type - let onChange: @Sendable ((any Sendable)?) -> Void - init( - key: String, - type: Any.Type, - onChange: @escaping @Sendable ((any Sendable)?) -> Void - ) { - self.key = key - self.type = type - self.onChange = onChange - super.init() - NotificationCenter.default.addObserver( - self, selector: #selector(onNotification), - name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, - object: NSUbiquitousKeyValueStore.default - ) - } + return UserDefaults.Dependency { key, type in + userDefaults.value.getSendable(forKey: key, as: type) + } set: { + userDefaults.value.setSendable($0, forKey: $1) + // NSUbiquitousKeyValueStore doesn't support KVO, so we call directly + // the continuation for local changes. + subject.yield(($1, $0)) + userDefaults.value.synchronize() + } values: { key, type in + AsyncStream((any Sendable)?.self) { continuation in + final class Observer: NSObject, Sendable { + let key: String + let type: Any.Type + let onChange: @Sendable ((any Sendable)?) -> Void + init( + key: String, + type: Any.Type, + onChange: @escaping @Sendable ((any Sendable)?) -> Void + ) { + self.key = key + self.type = type + self.onChange = onChange + super.init() + NotificationCenter.default.addObserver( + self, selector: #selector(onNotification), + name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, + object: NSUbiquitousKeyValueStore.default + ) + } - @objc func onNotification(_ notification: Notification) { - guard - let store = notification.object as? NSUbiquitousKeyValueStore, - let keys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String], - keys.contains(key) - else { return } - self.onChange(store.getSendable(forKey: self.key, as: self.type)) + @objc func onNotification(_ notification: Notification) { + guard + let store = notification.object as? NSUbiquitousKeyValueStore, + let keys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] + as? [String], + keys.contains(key) + else { return } + self.onChange(store.getSendable(forKey: self.key, as: self.type)) + } } - } - let object = Observer(key: key, type: type) { - subject.yield((key, $0)) - } + let object = Observer(key: key, type: type) { + subject.yield((key, $0)) + } - let task = Task { - for await tuple in subject.stream(bufferingPolicy: .bufferingNewest(0)) { - if tuple.0 == key { - continuation.yield(tuple.1) + let task = Task { + for await tuple in subject.stream(bufferingPolicy: .bufferingNewest(0)) { + if tuple.0 == key { + continuation.yield(tuple.1) + } } } - } - continuation.onTermination = { [object] _ in - task.cancel() - let _ = object + continuation.onTermination = { [object] _ in + task.cancel() + let _ = object + } } } } } -} #endif diff --git a/Tests/DeviceDependencyTests/DeviceDependencyTests.swift b/Tests/DeviceDependencyTests/DeviceDependencyTests.swift index cf58c4e..dbeefd4 100644 --- a/Tests/DeviceDependencyTests/DeviceDependencyTests.swift +++ b/Tests/DeviceDependencyTests/DeviceDependencyTests.swift @@ -14,115 +14,115 @@ import XCTest XCTAssertEqual(device.batteryLevel, 42) } } -#if DEBUG - @MainActor - func testFailingTestDeviceIOS_name() { - XCTExpectFailure { - let _ = device.name - } - } - @MainActor - func testFailingTestDeviceIOS_model() { - XCTExpectFailure { - let _ = device.model - } - } - @MainActor - func testFailingTestDeviceIOS_localizedModel() { - XCTExpectFailure { - let _ = device.localizedModel - } - } - @MainActor - func testFailingTestDeviceIOS_systemName() { - XCTExpectFailure { - let _ = device.systemName - } - } - @MainActor - func testFailingTestDeviceIOS_systemVersion() { - XCTExpectFailure { - let _ = device.systemVersion - } - } - @MainActor - func testFailingTestDeviceIOS_identifierForVendor() { - XCTExpectFailure { - let _ = device.identifierForVendor - } - } - @MainActor - func testFailingTestDeviceIOS_orientation() { - XCTExpectFailure { - let _ = device.orientation - } - } - @MainActor - func testFailingTestDeviceIOS_isGeneratingDeviceOrientationNotifications() { - XCTExpectFailure { - let _ = device.isGeneratingDeviceOrientationNotifications - } - } - @MainActor - func testFailingTestDeviceIOS_beginGeneratingDeviceOrientationNotifications() { - XCTExpectFailure { - let _ = device.beginGeneratingDeviceOrientationNotifications() - } - } - @MainActor - func testFailingTestDeviceIOS_endGeneratingDeviceOrientationNotifications() { - XCTExpectFailure { - let _ = device.endGeneratingDeviceOrientationNotifications() + #if DEBUG + @MainActor + func testFailingTestDeviceIOS_name() { + XCTExpectFailure { + let _ = device.name + } + } + @MainActor + func testFailingTestDeviceIOS_model() { + XCTExpectFailure { + let _ = device.model + } + } + @MainActor + func testFailingTestDeviceIOS_localizedModel() { + XCTExpectFailure { + let _ = device.localizedModel + } + } + @MainActor + func testFailingTestDeviceIOS_systemName() { + XCTExpectFailure { + let _ = device.systemName + } + } + @MainActor + func testFailingTestDeviceIOS_systemVersion() { + XCTExpectFailure { + let _ = device.systemVersion + } + } + @MainActor + func testFailingTestDeviceIOS_identifierForVendor() { + XCTExpectFailure { + let _ = device.identifierForVendor + } + } + @MainActor + func testFailingTestDeviceIOS_orientation() { + XCTExpectFailure { + let _ = device.orientation + } + } + @MainActor + func testFailingTestDeviceIOS_isGeneratingDeviceOrientationNotifications() { + XCTExpectFailure { + let _ = device.isGeneratingDeviceOrientationNotifications + } + } + @MainActor + func testFailingTestDeviceIOS_beginGeneratingDeviceOrientationNotifications() { + XCTExpectFailure { + let _ = device.beginGeneratingDeviceOrientationNotifications() + } + } + @MainActor + func testFailingTestDeviceIOS_endGeneratingDeviceOrientationNotifications() { + XCTExpectFailure { + let _ = device.endGeneratingDeviceOrientationNotifications() + } + } + @MainActor + func testFailingTestDeviceIOS_isBatteryMonitoringEnabled() { + XCTExpectFailure { + let _ = device.isBatteryMonitoringEnabled + } + } + @MainActor + func testFailingTestDeviceIOS_batteryState() { + XCTExpectFailure { + let _ = device.batteryState + } + } + @MainActor + func testFailingTestDeviceIOS_batteryLevel() { + XCTExpectFailure { + let _ = device.batteryLevel + } + } + @MainActor + func testFailingTestDeviceIOS_isProximityMonitoringEnabled() { + XCTExpectFailure { + let _ = device.isProximityMonitoringEnabled + } + } + @MainActor + func testFailingTestDeviceIOS_proximityState() { + XCTExpectFailure { + let _ = device.proximityState + } + } + @MainActor + func testFailingTestDeviceIOS_isMultitaskingSupported() { + XCTExpectFailure { + let _ = device.isMultitaskingSupported + } + } + @MainActor + func testFailingTestDeviceIOS_userInterfaceIdiom() { + XCTExpectFailure { + let _ = device.userInterfaceIdiom + } + } + @MainActor + func testFailingTestDeviceIOS_playInputClick() { + XCTExpectFailure { + let _ = device.playInputClick() + } } - } - @MainActor - func testFailingTestDeviceIOS_isBatteryMonitoringEnabled() { - XCTExpectFailure { - let _ = device.isBatteryMonitoringEnabled - } - } - @MainActor - func testFailingTestDeviceIOS_batteryState() { - XCTExpectFailure { - let _ = device.batteryState - } - } - @MainActor - func testFailingTestDeviceIOS_batteryLevel() { - XCTExpectFailure { - let _ = device.batteryLevel - } - } - @MainActor - func testFailingTestDeviceIOS_isProximityMonitoringEnabled() { - XCTExpectFailure { - let _ = device.isProximityMonitoringEnabled - } - } - @MainActor - func testFailingTestDeviceIOS_proximityState() { - XCTExpectFailure { - let _ = device.proximityState - } - } - @MainActor - func testFailingTestDeviceIOS_isMultitaskingSupported() { - XCTExpectFailure { - let _ = device.isMultitaskingSupported - } - } - @MainActor - func testFailingTestDeviceIOS_userInterfaceIdiom() { - XCTExpectFailure { - let _ = device.userInterfaceIdiom - } - } - @MainActor - func testFailingTestDeviceIOS_playInputClick() { - XCTExpectFailure { - let _ = device.playInputClick() - } - } #endif } #endif @@ -132,127 +132,127 @@ import XCTest final class DeviceDependencyTests: XCTestCase { @Dependency(\.device) var device -#if DEBUG - @MainActor - func testFailingTestDeviceWatchOS_name() { - XCTExpectFailure { - let _ = device.name - } - } - @MainActor - func testFailingTestDeviceWatchOS_model() { - XCTExpectFailure { - let _ = device.model - } - } - @MainActor - func testFailingTestDeviceWatchOS_localizedModel() { - XCTExpectFailure { - let _ = device.localizedModel - } - } - @MainActor - func testFailingTestDeviceWatchOS_systemName() { - XCTExpectFailure { - let _ = device.systemName - } - } - @MainActor - func testFailingTestDeviceWatchOS_systemVersion() { - XCTExpectFailure { - let _ = device.systemVersion - } - } - @MainActor - func testFailingTestDeviceWatchOS_identifierForVendor() { - XCTExpectFailure { - let _ = device.identifierForVendor - } - } - @MainActor - func testFailingTestDeviceWatchOS_screenBounds() { - XCTExpectFailure { - let _ = device.screenBounds - } - } - @MainActor - func testFailingTestDeviceWatchOS_screenScale() { - XCTExpectFailure { - let _ = device.screenScale - } - } - @MainActor - func testFailingTestDeviceWatchOS_preferredContentSizeCategory() { - XCTExpectFailure { - let _ = device.preferredContentSizeCategory - } - } - @MainActor - func testFailingTestDeviceWatchOS_layoutDirection() { - XCTExpectFailure { - let _ = device.layoutDirection - } - } - @MainActor - func testFailingTestDeviceWatchOS_wristLocation() { - XCTExpectFailure { - let _ = device.wristLocation + #if DEBUG + @MainActor + func testFailingTestDeviceWatchOS_name() { + XCTExpectFailure { + let _ = device.name + } + } + @MainActor + func testFailingTestDeviceWatchOS_model() { + XCTExpectFailure { + let _ = device.model + } + } + @MainActor + func testFailingTestDeviceWatchOS_localizedModel() { + XCTExpectFailure { + let _ = device.localizedModel + } + } + @MainActor + func testFailingTestDeviceWatchOS_systemName() { + XCTExpectFailure { + let _ = device.systemName + } + } + @MainActor + func testFailingTestDeviceWatchOS_systemVersion() { + XCTExpectFailure { + let _ = device.systemVersion + } + } + @MainActor + func testFailingTestDeviceWatchOS_identifierForVendor() { + XCTExpectFailure { + let _ = device.identifierForVendor + } + } + @MainActor + func testFailingTestDeviceWatchOS_screenBounds() { + XCTExpectFailure { + let _ = device.screenBounds + } + } + @MainActor + func testFailingTestDeviceWatchOS_screenScale() { + XCTExpectFailure { + let _ = device.screenScale + } + } + @MainActor + func testFailingTestDeviceWatchOS_preferredContentSizeCategory() { + XCTExpectFailure { + let _ = device.preferredContentSizeCategory + } + } + @MainActor + func testFailingTestDeviceWatchOS_layoutDirection() { + XCTExpectFailure { + let _ = device.layoutDirection + } + } + @MainActor + func testFailingTestDeviceWatchOS_wristLocation() { + XCTExpectFailure { + let _ = device.wristLocation + } + } + @MainActor + func testFailingTestDeviceWatchOS_crownOrientation() { + XCTExpectFailure { + let _ = device.crownOrientation + } + } + @MainActor + func testFailingTestDeviceWatchOS_isBatteryMonitoringEnabled() { + XCTExpectFailure { + let _ = device.isBatteryMonitoringEnabled + } + } + @MainActor + func testFailingTestDeviceWatchOS_batteryState() { + XCTExpectFailure { + let _ = device.batteryState + } + } + @MainActor + func testFailingTestDeviceWatchOS_batteryLevel() { + XCTExpectFailure { + let _ = device.batteryLevel + } + } + @MainActor + func testFailingTestDeviceWatchOS_waterResistanceRating() { + XCTExpectFailure { + let _ = device.waterResistanceRating + } + } + @MainActor + func testFailingTestDeviceWatchOS_isWaterLockEnabled() { + XCTExpectFailure { + let _ = device.isWaterLockEnabled + } + } + @MainActor + func testFailingTestDeviceWatchOS_supportsAudioStreaming() { + XCTExpectFailure { + let _ = device.supportsAudioStreaming + } + } + @MainActor + func testFailingTestDeviceWatchOS_play() { + XCTExpectFailure { + let _ = device.play(.click) + } + } + @MainActor + func testFailingTestDeviceWatchOS_enableWaterLock() { + XCTExpectFailure { + let _ = device.enableWaterLock() + } } - } - @MainActor - func testFailingTestDeviceWatchOS_crownOrientation() { - XCTExpectFailure { - let _ = device.crownOrientation - } - } - @MainActor - func testFailingTestDeviceWatchOS_isBatteryMonitoringEnabled() { - XCTExpectFailure { - let _ = device.isBatteryMonitoringEnabled - } - } - @MainActor - func testFailingTestDeviceWatchOS_batteryState() { - XCTExpectFailure { - let _ = device.batteryState - } - } - @MainActor - func testFailingTestDeviceWatchOS_batteryLevel() { - XCTExpectFailure { - let _ = device.batteryLevel - } - } - @MainActor - func testFailingTestDeviceWatchOS_waterResistanceRating() { - XCTExpectFailure { - let _ = device.waterResistanceRating - } - } - @MainActor - func testFailingTestDeviceWatchOS_isWaterLockEnabled() { - XCTExpectFailure { - let _ = device.isWaterLockEnabled - } - } - @MainActor - func testFailingTestDeviceWatchOS_supportsAudioStreaming() { - XCTExpectFailure { - let _ = device.supportsAudioStreaming - } - } - @MainActor - func testFailingTestDeviceWatchOS_play() { - XCTExpectFailure { - let _ = device.play(.click) - } - } - @MainActor - func testFailingTestDeviceWatchOS_enableWaterLock() { - XCTExpectFailure { - let _ = device.enableWaterLock() - } - } #endif } #endif