From 17fddec4d997f4a2996d60ab8cd26d1a2ad0f060 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 29 Mar 2024 01:45:39 +0900 Subject: [PATCH] Various tweaks --- .github/workflows/main.yml | 4 +- Package.swift | 2 +- Sources/Defaults/Defaults+Extensions.swift | 2 +- Sources/Defaults/Defaults+Protocol.swift | 161 +++++-- Sources/Defaults/Defaults+iCloud.swift | 428 +++++++++--------- Sources/Defaults/Defaults.swift | 118 +---- .../Documentation.docc/Documentation.md | 2 +- Sources/Defaults/Observation+Combine.swift | 6 +- Sources/Defaults/Observation.swift | 5 +- Sources/Defaults/Utilities.swift | 6 +- .../DefaultsTests/Defaults+iCloudTests.swift | 42 +- .../DefaultsNSSecureCodingTests.swift | 8 +- Tests/DefaultsTests/DefaultsTests.swift | 8 +- readme.md | 2 +- 14 files changed, 404 insertions(+), 390 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a5a98c..4513e40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,10 +4,10 @@ on: - pull_request jobs: test: - runs-on: macos-13 + runs-on: macos-14 steps: - uses: actions/checkout@v4 - - run: sudo xcode-select -switch /Applications/Xcode_15.2.app + - run: sudo xcode-select -switch /Applications/Xcode_15.3.app - run: swift test lint: runs-on: ubuntu-latest diff --git a/Package.swift b/Package.swift index 63ffcf0..fe3d4bf 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 import PackageDescription let package = Package( diff --git a/Sources/Defaults/Defaults+Extensions.swift b/Sources/Defaults/Defaults+Extensions.swift index c14ee6b..38e73aa 100644 --- a/Sources/Defaults/Defaults+Extensions.swift +++ b/Sources/Defaults/Defaults+Extensions.swift @@ -171,7 +171,7 @@ extension UserDefaults: DefaultsKeyValueStore {} extension DefaultsLockProtocol { @discardableResult func with(_ body: @Sendable () throws -> R) rethrows -> R where R: Sendable { - self.lock() + lock() defer { self.unlock() } diff --git a/Sources/Defaults/Defaults+Protocol.swift b/Sources/Defaults/Defaults+Protocol.swift index 421621c..f6d08dc 100644 --- a/Sources/Defaults/Defaults+Protocol.swift +++ b/Sources/Defaults/Defaults+Protocol.swift @@ -1,56 +1,151 @@ import Foundation -public protocol _DefaultsSerializable { - typealias Value = Bridge.Value - typealias Serializable = Bridge.Serializable - associatedtype Bridge: Defaults.Bridge - +extension Defaults { /** - Static bridge for the `Value` which cannot be stored natively. - */ - static var bridge: Bridge { get } + Types that conform to this protocol can be used with `Defaults`. - /** - A flag to determine whether `Value` can be stored natively or not. + The type should have a static variable `bridge` which should reference an instance of a type that conforms to `Defaults.Bridge`. + + ```swift + struct User { + username: String + password: String + } + + extension User: Defaults.Serializable { + static let bridge = UserBridge() + } + ``` */ - static var isNativelySupportedType: Bool { get } + public protocol Serializable { + typealias Value = Bridge.Value + typealias Serializable = Bridge.Serializable + associatedtype Bridge: Defaults.Bridge + + /** + Static bridge for the `Value` which cannot be stored natively. + */ + static var bridge: Bridge { get } + + /** + A flag to determine whether `Value` can be stored natively or not. + */ + static var isNativelySupportedType: Bool { get } + } } -public protocol _DefaultsBridge { - associatedtype Value - associatedtype Serializable +extension Defaults { + public protocol Bridge { + associatedtype Value + associatedtype Serializable - func serialize(_ value: Value?) -> Serializable? - func deserialize(_ object: Serializable?) -> Value? + func serialize(_ value: Value?) -> Serializable? + func deserialize(_ object: Serializable?) -> Value? + } } -public protocol _DefaultsCollectionSerializable: Collection, Defaults.Serializable { +extension Defaults { /** - `Collection` does not have a initializer, but we need a initializer to convert an array into the `Value`. - */ - init(_ elements: [Element]) -} + Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities. + + ```swift + enum Interval: Int, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable { + case tenMinutes = 10 + case halfHour = 30 + case oneHour = 60 + } + ``` + + By default, if an `enum` conforms to `Codable` and `Defaults.Serializable`, it will use the `CodableBridge`, but by conforming to `Defaults.PreferRawRepresentable`, we can switch the bridge back to `RawRepresentableBridge`. + */ + public protocol PreferRawRepresentable: RawRepresentable {} -public protocol _DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable { /** - Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly. + Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities. */ - func toArray() -> [Element] + public protocol PreferNSSecureCoding: NSObject, NSSecureCoding {} } -public protocol _DefaultsCodableBridge: Defaults.Bridge where Serializable == String, Value: Codable {} +extension Defaults { + public protocol CollectionSerializable: Collection, Serializable { + /** + `Collection` does not have a initializer, but we need a initializer to convert an array into the `Value`. + */ + init(_ elements: [Element]) + } -public protocol _DefaultsPreferRawRepresentable: RawRepresentable {} -public protocol _DefaultsPreferNSSecureCoding: NSObject, NSSecureCoding {} + public protocol SetAlgebraSerializable: SetAlgebra, Serializable { + /** + Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly. + */ + func toArray() -> [Element] + } -// Essential properties for serializing and deserializing `ClosedRange` and `Range`. -public protocol _DefaultsRange { - associatedtype Bound: Comparable, Defaults.Serializable + public protocol CodableBridge: Bridge where Serializable == String, Value: Codable {} - var lowerBound: Bound { get } - var upperBound: Bound { get } + // Essential properties for serializing and deserializing `ClosedRange` and `Range`. + public protocol Range { + associatedtype Bound: Comparable, Defaults.Serializable - init(uncheckedBounds: (lower: Bound, upper: Bound)) + var lowerBound: Bound { get } + var upperBound: Bound { get } + + init(uncheckedBounds: (lower: Bound, upper: Bound)) + } + + /** + A `Bridge` is responsible for serialization and deserialization. + + It has two associated types `Value` and `Serializable`. + + - `Value`: The type you want to use. + - `Serializable`: The type stored in `UserDefaults`. + - `serialize`: Executed before storing to the `UserDefaults` . + - `deserialize`: Executed after retrieving its value from the `UserDefaults`. + + ```swift + struct User { + username: String + password: String + } + + extension User { + static let bridge = UserBridge() + } + + struct UserBridge: Defaults.Bridge { + typealias Value = User + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value else { + return nil + } + + return [ + "username": value.username, + "password": value.password + ] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object, + let username = object["username"], + let password = object["password"] + else { + return nil + } + + return User( + username: username, + password: password + ) + } + } + ``` + */ + public typealias RangeSerializable = Defaults.Range & Serializable } /** diff --git a/Sources/Defaults/Defaults+iCloud.swift b/Sources/Defaults/Defaults+iCloud.swift index cb624c9..6a38ee9 100644 --- a/Sources/Defaults/Defaults+iCloud.swift +++ b/Sources/Defaults/Defaults+iCloud.swift @@ -6,6 +6,196 @@ import UIKit #endif import Combine import Foundation +#if os(watchOS) +import WatchKit +#endif + +extension Defaults { + /** + Synchronize values across devices using iCloud. + + To synchronize a key with iCloud, set `iCloud: true` on the ``Defaults/Key``. That's it! ✨ + + ```swift + import Defaults + + extension Defaults.Keys { + static let isUnicornMode = Key("isUnicornMode", default: false, iCloud: true) + } + + // … + + // This change will now be synced to other devices. + Defaults[.isUnicornMode] = true + ``` + + - Important: You need to enable the `iCloud` capability in the “Signing and Capabilities” tab in Xcode and then enable “Key-value storage” in the iCloud services. + + ## Notes + + - If there is a conflict, it will use the latest change. + - Max 1024 keys and a total of 1 MB storage. + - It uses [`NSUbiquitousKeyValueStore`](https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore) internally. + + ## Dynamically Toggle Syncing + + You can also toggle the syncing behavior dynamically using the ``Defaults/iCloud/add(_:)-5gffb`` and ``Defaults/iCloud/remove(_:)-1b8w5`` methods. + + ```swift + import Defaults + + extension Defaults.Keys { + static let isUnicornMode = Key("isUnicornMode", default: false) + } + + // … + + if shouldSync { + Defaults.iCloud.add(.isUnicornMode) + } + ``` + */ + public enum iCloud { + /** + The singleton for Defaults's iCloudSynchronizer. + */ + static var synchronizer = iCloudSynchronizer(remoteStorage: NSUbiquitousKeyValueStore.default) + + /** + The synced keys. + */ + public static var keys: Set { synchronizer.keys } + + // Note: Can be made public if someone shows a real use-case for it. + /** + Enable this if you want the key to be synced right away when it's changed. + */ + static var syncOnChange = true + + /** + Log debug info about the syncing. + + It will include details such as the key being synced, its corresponding value, and the status of the synchronization. + */ + public static var isDebug = false + + /** + Add the keys to be automatically synced. + */ + public static func add(_ keys: Defaults.Keys...) { + synchronizer.add(keys) + } + + /** + Add the keys to be automatically synced. + */ + public static func add(_ keys: [Defaults.Keys]) { + synchronizer.add(keys) + } + + /** + Remove the keys that are set to be automatically synced. + */ + public static func remove(_ keys: Defaults.Keys...) { + synchronizer.remove(keys) + } + + /** + Remove the keys that are set to be automatically synced. + */ + public static func remove(_ keys: [Defaults.Keys]) { + synchronizer.remove(keys) + } + + /** + Remove all keys that are set to be automatically synced. + */ + public static func removeAll() { + synchronizer.removeAll() + } + + /** + Waits for the completion of synchronization. + + You generally don't need this as synchronization is automatic, but in some cases it could be useful to not continue until all values are synchronized to the cloud. + + ```swift + import Defaults + + extension Defaults.Keys { + static let isUnicornMode = Key("isUnicornMode", default: false, iCloud: true) + } + + // … + + Task { + Defaults[.isUnicornMode] = true + + print(Defaults[.isUnicornMode]) + //=> true + + await Defaults.iCloud.waitForSyncCompletion() + + // The value is now synchronized to the cloud too. + } + ``` + */ + public static func waitForSyncCompletion() async { + await synchronizer.sync() + } + + // Only make these public if there is an actual need. + // https://github.com/sindresorhus/Defaults/pull/136#discussion_r1544546756 + + /** + Create synchronization tasks for all the keys that have been added to the ``Defaults/iCloud``. + */ + static func syncWithoutWaiting() { + synchronizer.syncWithoutWaiting() + } + + /** + Create synchronization tasks for the specified `keys` from the given source, which can be either a remote server or a local cache. + + - Parameter keys: The keys that should be synced. + - Parameter source: Sync keys from which data source(remote or local) + + - Note: `source` should be specified if `key` has not been added to ``Defaults/iCloud``. + */ + static func syncWithoutWaiting(_ keys: Defaults.Keys..., source: DataSource? = nil) { + synchronizer.syncWithoutWaiting(keys, source) + } + + /** + Create synchronization tasks for the specified `keys` from the given source, which can be either a remote server or a local cache. + + - Parameter keys: The keys that should be synced. + - Parameter source: Sync keys from which data source(remote or local) + + - Note: `source` should be specified if `key` has not been added to ``Defaults/iCloud``. + */ + static func syncWithoutWaiting(_ keys: [Defaults.Keys], source: DataSource? = nil) { + synchronizer.syncWithoutWaiting(keys, source) + } + } +} + +extension Defaults.iCloud { + /** + Represent different data sources available for synchronization. + */ + public enum DataSource { + /** + Using `key.suite` as data source. + */ + case local + + /** + Using `NSUbiquitousKeyValueStore` as data source. + */ + case remote + } +} private enum SyncStatus { case idle @@ -31,7 +221,7 @@ final class iCloudSynchronizer { @TaskLocal static var timestamp: Date? - private var cancellables: Set = [] + private var cancellables = Set() /** Key for recording the synchronization between `NSUbiquitousKeyValueStore` and `UserDefaults`. @@ -63,14 +253,14 @@ final class iCloudSynchronizer { guard let self, let suite = observable.suite, - let key = self.keys.first(where: { $0.name == observable.key && $0.suite == suite }), + let key = keys.first(where: { $0.name == observable.key && $0.suite == suite }), // Prevent triggering local observation when syncing from remote. - !self.remoteSyncingKeys.contains(key) + !remoteSyncingKeys.contains(key) else { return } - self.enqueue { + enqueue { self.recordTimestamp(forKey: key, timestamp: Self.timestamp, source: .local) await self.syncKey(key, source: .local) } @@ -81,7 +271,7 @@ final class iCloudSynchronizer { */ func add(_ keys: [Defaults.Keys]) { self.keys.formUnion(keys) - self.syncWithoutWaiting(keys) + syncWithoutWaiting(keys) for key in keys { localKeysMonitor.addObserver(key) } @@ -119,12 +309,12 @@ final class iCloudSynchronizer { - Parameter keys: If the keys parameter is an empty array, the method will use the keys that were added to `Defaults.iCloud`. - Parameter source: Sync keys from which data source (remote or local). */ - func syncWithoutWaiting(_ keys: [Defaults.Keys] = [], _ source: Defaults.DataSource? = nil) { + func syncWithoutWaiting(_ keys: [Defaults.Keys] = [], _ source: Defaults.iCloud.DataSource? = nil) { let keys = keys.isEmpty ? Array(self.keys) : keys for key in keys { let latest = source ?? latestDataSource(forKey: key) - self.enqueue { + enqueue { await self.syncKey(key, source: latest) } } @@ -141,7 +331,7 @@ final class iCloudSynchronizer { Enqueue the synchronization task into `backgroundQueue` with the current timestamp. */ private func enqueue(_ task: @escaping TaskQueue.AsyncTask) { - self.backgroundQueue.async { + backgroundQueue.async { await Self.$timestamp.withValue(Date()) { await task() } @@ -154,7 +344,7 @@ final class iCloudSynchronizer { - Parameter key: The key to synchronize. - Parameter source: Sync key from which data source (remote or local). */ - private func syncKey(_ key: Defaults.Keys, source: Defaults.DataSource) async { + private func syncKey(_ key: Defaults.Keys, source: Defaults.iCloud.DataSource) async { Self.logKeySyncStatus(key, source: source, syncStatus: .idle) switch source { @@ -227,7 +417,7 @@ final class iCloudSynchronizer { The timestamp storage format varies across different source providers due to storage limitations. */ - private func timestamp(forKey key: Defaults.Keys, source: Defaults.DataSource) -> Date? { + private func timestamp(forKey key: Defaults.Keys, source: Defaults.iCloud.DataSource) -> Date? { switch source { case .remote: guard @@ -252,7 +442,7 @@ final class iCloudSynchronizer { /** Mark the current timestamp to the given storage. */ - func recordTimestamp(forKey key: Defaults.Keys, timestamp: Date?, source: Defaults.DataSource) { + func recordTimestamp(forKey key: Defaults.Keys, timestamp: Date?, source: Defaults.iCloud.DataSource) { switch source { case .remote: guard @@ -275,12 +465,12 @@ final class iCloudSynchronizer { /** Determine which data source has the latest data available by comparing the timestamps of the local and remote sources. */ - private func latestDataSource(forKey key: Defaults.Keys) -> Defaults.DataSource { + private func latestDataSource(forKey key: Defaults.Keys) -> Defaults.iCloud.DataSource { // If the remote timestamp does not exist, use the local timestamp as the latest data source. - guard let remoteTimestamp = self.timestamp(forKey: key, source: .remote) else { + guard let remoteTimestamp = timestamp(forKey: key, source: .remote) else { return .local } - guard let localTimestamp = self.timestamp(forKey: key, source: .local) else { + guard let localTimestamp = timestamp(forKey: key, source: .local) else { return .remote } @@ -288,9 +478,7 @@ final class iCloudSynchronizer { } } -/** -`iCloudSynchronizer` notification related functions. -*/ +// Notification related functions. extension iCloudSynchronizer { private func registerNotifications() { // TODO: Replace it with async stream when Swift supports custom executors. @@ -301,27 +489,28 @@ extension iCloudSynchronizer { return } - self.didChangeExternally(notification: notification) + didChangeExternally(notification: notification) } .store(in: &cancellables) - // TODO: Replace it with async stream when Swift supports custom executors. - #if os(iOS) || os(tvOS) || os(visionOS) - NotificationCenter.default - .publisher(for: UIScene.willEnterForegroundNotification) - #elseif os(watchOS) - NotificationCenter.default - .publisher(for: WKExtension.applicationWillEnterForegroundNotification) + #if canImport(UIKit) + #if os(watchOS) + let notificationName = WKExtension.applicationWillEnterForegroundNotification + #else + let notificationName = UIScene.willEnterForegroundNotification #endif - #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + + // TODO: Replace it with async stream when Swift supports custom executors. + NotificationCenter.default + .publisher(for: notificationName) .sink { [weak self] notification in guard let self else { return } - self.willEnterForeground(notification: notification) + willEnterForeground(notification: notification) } - .store(in: cancellables) + .store(in: &cancellables) #endif } @@ -343,7 +532,7 @@ extension iCloudSynchronizer { return } - for key in self.keys where changedKeys.contains(key.name) { + for key in keys where changedKeys.contains(key.name) { guard let remoteTimestamp = self.timestamp(forKey: key, source: .remote) else { continue } @@ -361,14 +550,16 @@ extension iCloudSynchronizer { } } -/** -`iCloudSynchronizer` logging related functions. -*/ +// Logging related functions. extension iCloudSynchronizer { - @available(macOS 11, iOS 14, tvOS 14, watchOS 7, visionOS 1.0, *) private static let logger = Logger(OSLog.default) - private static func logKeySyncStatus(_ key: Defaults.Keys, source: Defaults.DataSource, syncStatus: SyncStatus, value: Any? = nil) { + private static func logKeySyncStatus( + _ key: Defaults.Keys, + source: Defaults.iCloud.DataSource, + syncStatus: SyncStatus, + value: Any? = nil + ) { guard Defaults.iCloud.isDebug else { return } @@ -401,173 +592,6 @@ extension iCloudSynchronizer { return } - if #available(macOS 11, iOS 14, tvOS 14, watchOS 7, visionOS 1.0, *) { - logger.debug("[Defaults.iCloud] \(message)") - } else { - #if canImport(OSLog) - os_log(.debug, log: .default, "[Defaults.iCloud] %@", message) - #else - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSSZZZ" - let dateString = dateFormatter.string(from: Date()) - let processName = ProcessInfo.processInfo.processName - let processIdentifier = ProcessInfo.processInfo.processIdentifier - var threadID: UInt64 = 0 - pthread_threadid_np(nil, &threadID) - print("\(dateString) \(processName)[\(processIdentifier):\(threadID)] [Defaults.iCloud] \(message)") - #endif - } - } -} - -extension Defaults { - /** - Represent different data sources available for synchronization. - */ - public enum DataSource { - /** - Using `key.suite` as data source. - */ - case local - - /** - Using `NSUbiquitousKeyValueStore` as data source. - */ - case remote - } - - /** - Synchronize values with different devices over iCloud. - - There are five ways to initiate synchronization, each of which will create a synchronization task in ``Defaults/iCloud/iCloud``: - - 1. Using ``iCloud/add(_:)-5gffb`` - 2. Utilizing ``iCloud/syncWithoutWaiting(_:source:)-9cpju`` - 3. Observing UserDefaults for added ``Defaults/Defaults/Key`` using Key-Value Observation (KVO) - 4. Monitoring `NSUbiquitousKeyValueStore.didChangeExternallyNotification` for added ``Defaults/Defaults/Key``. - 5. Initializing ``Defaults/Defaults/Keys`` with parameter `iCloud: true`. - - > Tip: After initializing the task, we can call ``iCloud/sync()`` to ensure that all tasks in the backgroundQueue are completed. - - ```swift - import Defaults - - extension Defaults.Keys { - static let isUnicornMode = Key("isUnicornMode", default: true, iCloud: true) - } - - Task { - let quality = Defaults.Key("quality", default: 0) - Defaults.iCloud.add(quality) - await Defaults.iCloud.sync() // Optional step: only needed if you require everything to be synced before continuing. - // Both `isUnicornMode` and `quality` are synced. - } - ``` - */ - public enum iCloud { - /** - The singleton for Defaults's iCloudSynchronizer. - */ - static var synchronizer = iCloudSynchronizer(remoteStorage: NSUbiquitousKeyValueStore.default) - - /** - The synced keys. - */ - public static var keys: Set { synchronizer.keys } - - /** - Enable this if you want to call ```` when a value is changed. - */ - public static var syncOnChange = false - - /** - Enable this if you want to debug the syncing status of keys. - Logs will be printed to the console in OSLog format. - - - Note: The log information will include details such as the key being synced, its corresponding value, and the status of the synchronization. - */ - public static var isDebug = false - - /** - Add the keys to be automatically synced. - */ - public static func add(_ keys: Defaults.Keys...) { - synchronizer.add(keys) - } - - /** - Add the keys to be automatically synced. - */ - public static func add(_ keys: [Defaults.Keys]) { - synchronizer.add(keys) - } - - /** - Remove the keys that are set to be automatically synced. - */ - public static func remove(_ keys: Defaults.Keys...) { - synchronizer.remove(keys) - } - - /** - Remove the keys that are set to be automatically synced. - */ - public static func remove(_ keys: [Defaults.Keys]) { - synchronizer.remove(keys) - } - - /** - Remove all keys that are set to be automatically synced. - */ - public static func removeAll() { - synchronizer.removeAll() - } - - /** - Explicitly synchronizes in-memory keys and values with those stored on disk. - - As per apple docs, the only recommended time to call this method is upon app launch, or upon returning to the foreground, to ensure that the in-memory key-value store representation is up-to-date. - */ - public static func synchronize() { - synchronizer.synchronize() - } - - /** - Wait until synchronization is complete. - */ - public static func sync() async { - await synchronizer.sync() - } - - /** - Create synchronization tasks for all the keys that have been added to the ``Defaults/Defaults/iCloud``. - */ - public static func syncWithoutWaiting() { - synchronizer.syncWithoutWaiting() - } - - /** - Create synchronization tasks for the specified `keys` from the given source, which can be either a remote server or a local cache. - - - Parameter keys: The keys that should be synced. - - Parameter source: Sync keys from which data source(remote or local) - - - Note: `source` should be specified if `key` has not been added to ``Defaults/Defaults/iCloud``. - */ - public static func syncWithoutWaiting(_ keys: Defaults.Keys..., source: DataSource? = nil) { - synchronizer.syncWithoutWaiting(keys, source) - } - - /** - Create synchronization tasks for the specified `keys` from the given source, which can be either a remote server or a local cache. - - - Parameter keys: The keys that should be synced. - - Parameter source: Sync keys from which data source(remote or local) - - - Note: `source` should be specified if `key` has not been added to ``Defaults/Defaults/iCloud``. - */ - public static func syncWithoutWaiting(_ keys: [Defaults.Keys], source: DataSource? = nil) { - synchronizer.syncWithoutWaiting(keys, source) - } + logger.debug("[Defaults.iCloud] \(message)") } } diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index 2899e56..a63693d 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -67,6 +67,8 @@ extension Defaults { suite.removeObject(forKey: name) } } + + public typealias Keys = _AnyKey } extension Defaults { @@ -85,7 +87,7 @@ extension Defaults { } ``` - - Warning: The `UserDefaults` name must be ASCII, not start with `@`, and cannot contain a dot (`.`). + - Important: The `UserDefaults` name must be ASCII, not start with `@`, and cannot contain a dot (`.`). */ public final class Key: _AnyKey { /** @@ -103,7 +105,7 @@ extension Defaults { Create a key. - Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`). - - Parameter iCloud: Automatically synchronize the value with ``Defaults/Defaults/iCloud``. + - Parameter iCloud: Automatically synchronize the value with ``Defaults/iCloud``. The `default` parameter should not be used if the `Value` type is an optional. */ @@ -148,7 +150,7 @@ extension Defaults { ``` - Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`). - - Parameter iCloud: Automatically synchronize the value with ``Defaults/Defaults/iCloud``. + - Parameter iCloud: Automatically synchronize the value with ``Defaults/iCloud``. - Note: This initializer will not set the default value in the actual `UserDefaults`. This should not matter much though. It's only really useful if you use legacy KVO bindings. */ @@ -176,7 +178,7 @@ extension Defaults.Key { Create a key with an optional value. - Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`). - - Parameter iCloud: Automatically synchronize the value with ``Defaults/Defaults/iCloud``. + - Parameter iCloud: Automatically synchronize the value with ``Defaults/iCloud``. */ public convenience init( _ name: String, @@ -212,112 +214,6 @@ extension Defaults._AnyKey: Hashable { } } -extension Defaults { - public typealias Keys = _AnyKey - - /** - Types that conform to this protocol can be used with `Defaults`. - - The type should have a static variable `bridge` which should reference an instance of a type that conforms to `Defaults.Bridge`. - - ```swift - struct User { - username: String - password: String - } - - extension User: Defaults.Serializable { - static let bridge = UserBridge() - } - ``` - */ - public typealias Serializable = _DefaultsSerializable - - public typealias CollectionSerializable = _DefaultsCollectionSerializable - public typealias SetAlgebraSerializable = _DefaultsSetAlgebraSerializable - - /** - Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities. - - ```swift - enum Interval: Int, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable { - case tenMinutes = 10 - case halfHour = 30 - case oneHour = 60 - } - ``` - - By default, if an `enum` conforms to `Codable` and `Defaults.Serializable`, it will use the `CodableBridge`, but by conforming to `Defaults.PreferRawRepresentable`, we can switch the bridge back to `RawRepresentableBridge`. - */ - public typealias PreferRawRepresentable = _DefaultsPreferRawRepresentable - - /** - Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities. - */ - public typealias PreferNSSecureCoding = _DefaultsPreferNSSecureCoding - - /** - A `Bridge` is responsible for serialization and deserialization. - - It has two associated types `Value` and `Serializable`. - - - `Value`: The type you want to use. - - `Serializable`: The type stored in `UserDefaults`. - - `serialize`: Executed before storing to the `UserDefaults` . - - `deserialize`: Executed after retrieving its value from the `UserDefaults`. - - ```swift - struct User { - username: String - password: String - } - - extension User { - static let bridge = UserBridge() - } - - struct UserBridge: Defaults.Bridge { - typealias Value = User - typealias Serializable = [String: String] - - func serialize(_ value: Value?) -> Serializable? { - guard let value else { - return nil - } - - return [ - "username": value.username, - "password": value.password - ] - } - - func deserialize(_ object: Serializable?) -> Value? { - guard - let object, - let username = object["username"], - let password = object["password"] - else { - return nil - } - - return User( - username: username, - password: password - ) - } - } - ``` - */ - public typealias Bridge = _DefaultsBridge - - public typealias RangeSerializable = _DefaultsRange & _DefaultsSerializable - - /** - Convenience protocol for `Codable`. - */ - typealias CodableBridge = _DefaultsCodableBridge -} - extension Defaults { /** Observe updates to a stored value. @@ -371,7 +267,7 @@ extension Defaults { } ``` - - Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-9eh8`` if you need that. You could use [`merge`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md) to merge them into a single sequence. + - Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-88orv`` if you need that. You could use [`merge`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md) to merge them into a single sequence. */ public static func updates( _ keys: [_AnyKey], diff --git a/Sources/Defaults/Documentation.docc/Documentation.md b/Sources/Defaults/Documentation.docc/Documentation.md index 5147ba0..0ba7818 100644 --- a/Sources/Defaults/Documentation.docc/Documentation.md +++ b/Sources/Defaults/Documentation.docc/Documentation.md @@ -51,7 +51,7 @@ typealias Default = _Default ### Methods -- ``Defaults/updates(_:initial:)-9eh8`` +- ``Defaults/updates(_:initial:)-88orv`` - ``Defaults/updates(_:initial:)-1mqkb`` - ``Defaults/reset(_:)-7jv5v`` - ``Defaults/reset(_:)-7es1e`` diff --git a/Sources/Defaults/Observation+Combine.swift b/Sources/Defaults/Observation+Combine.swift index c045f3e..18a99c5 100644 --- a/Sources/Defaults/Observation+Combine.swift +++ b/Sources/Defaults/Observation+Combine.swift @@ -84,7 +84,7 @@ extension Defaults { } ``` - - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead. + - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-88orv`` instead. */ public static func publisher( _ key: Key, @@ -99,7 +99,7 @@ extension Defaults { /** Publisher for multiple `Key` observation, but without specific information about changes. - - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead. + - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-88orv`` instead. */ public static func publisher( keys: [_AnyKey], @@ -122,7 +122,7 @@ extension Defaults { /** Publisher for multiple `Key` observation, but without specific information about changes. - - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead. + - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-88orv`` instead. */ public static func publisher( keys: _AnyKey..., diff --git a/Sources/Defaults/Observation.swift b/Sources/Defaults/Observation.swift index dc06578..d9348f7 100644 --- a/Sources/Defaults/Observation.swift +++ b/Sources/Defaults/Observation.swift @@ -391,7 +391,7 @@ extension Defaults { } ``` - - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead. + - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-88orv`` instead. */ public static func observe( _ key: Key, @@ -407,7 +407,6 @@ extension Defaults { return observation } - /** Observe multiple keys of any type, but without any information about the changes. @@ -422,7 +421,7 @@ extension Defaults { } ``` - - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead. + - Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-88orv`` instead. */ public static func observe( keys: _AnyKey..., diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index d952c3f..a20835b 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -235,13 +235,14 @@ extension Defaults.Serializable { } } +// TODO: Remove this in favor of `MutexLock` when targeting Swift 6. // swiftlint:disable:next final_class class Lock: DefaultsLockProtocol { final class UnfairLock: Lock { private let _lock: os_unfair_lock_t override init() { - _lock = .allocate(capacity: 1) + self._lock = .allocate(capacity: 1) _lock.initialize(to: os_unfair_lock()) } @@ -360,13 +361,12 @@ final class TaskQueue { queueContinuation?.yield { continuation.resume() } - return } } } } -// TODO: replace with Swift 6 native Atomics support. +// TODO: Replace with Swift 6 native Atomics support: https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md?rgh-link-date=2024-03-29T14%3A14%3A00Z#changes-from-the-accepted-proposal @propertyWrapper final class Atomic { private let lock: Lock = .make() diff --git a/Tests/DefaultsTests/Defaults+iCloudTests.swift b/Tests/DefaultsTests/Defaults+iCloudTests.swift index 8becd44..4e3824c 100644 --- a/Tests/DefaultsTests/Defaults+iCloudTests.swift +++ b/Tests/DefaultsTests/Defaults+iCloudTests.swift @@ -83,7 +83,7 @@ final class DefaultsICloudTests: XCTestCase { Defaults.removeAll() } - private func updateMockStorage(key: String, value: T, _ date: Date? = nil) { + private func updateMockStorage(key: String, value: some Any, _ date: Date? = nil) { mockStorage.set([date ?? Date(), value], forKey: key) } @@ -93,7 +93,7 @@ final class DefaultsICloudTests: XCTestCase { let quality = Defaults.Key("testICloudInitialize_quality", default: 0.0, iCloud: true) print(Defaults.iCloud.keys) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "0") XCTAssertEqual(mockStorage.data(forKey: quality.name), 0.0) let name_expected = ["1", "2", "3", "4", "5", "6", "7"] @@ -102,7 +102,7 @@ final class DefaultsICloudTests: XCTestCase { for index in 0..("testDidChangeExternallyNotification_name", iCloud: true) let quality = Defaults.Key("testDidChangeExternallyNotification_quality", iCloud: true) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(Defaults[name], "0") XCTAssertEqual(Defaults[quality], 0.0) let name_expected = ["1", "2", "3", "4", "5", "6", "7"] @@ -144,19 +144,19 @@ final class DefaultsICloudTests: XCTestCase { updateMockStorage(key: quality.name, value: quality_expected[index]) mockStorage.synchronize() } - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(Defaults[name], "7") XCTAssertEqual(Defaults[quality], 7.0) Defaults[name] = "8" Defaults[quality] = 8.0 - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "8") XCTAssertEqual(mockStorage.data(forKey: quality.name), 8.0) Defaults[name] = nil Defaults[quality] = nil - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertNil(mockStorage.data(forKey: name.name)) XCTAssertNil(mockStorage.data(forKey: quality.name)) } @@ -174,7 +174,7 @@ final class DefaultsICloudTests: XCTestCase { XCTAssertEqual(Defaults[quality], quality_expected[index]) } - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "7") XCTAssertEqual(mockStorage.data(forKey: quality.name), 7.0) } @@ -184,14 +184,14 @@ final class DefaultsICloudTests: XCTestCase { let quality = Defaults.Key("testRemoveKey_quality", default: 0.0, iCloud: true) Defaults[name] = "1" Defaults[quality] = 1.0 - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "1") XCTAssertEqual(mockStorage.data(forKey: quality.name), 1.0) Defaults.iCloud.remove(quality) Defaults[name] = "2" Defaults[quality] = 1.0 - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "2") XCTAssertEqual(mockStorage.data(forKey: quality.name), 1.0) } @@ -206,7 +206,7 @@ final class DefaultsICloudTests: XCTestCase { Defaults[name] = name_expected[index] Defaults[quality] = quality_expected[index] Defaults.iCloud.syncWithoutWaiting(name, quality, source: .local) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), name_expected[index]) XCTAssertEqual(mockStorage.data(forKey: quality.name), quality_expected[index]) } @@ -214,7 +214,7 @@ final class DefaultsICloudTests: XCTestCase { updateMockStorage(key: name.name, value: "8") updateMockStorage(key: quality.name, value: 8) Defaults.iCloud.syncWithoutWaiting(name, quality, source: .remote) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(Defaults[quality], 8.0) XCTAssertEqual(Defaults[name], "8") } @@ -229,7 +229,7 @@ final class DefaultsICloudTests: XCTestCase { updateMockStorage(key: name.name, value: name_expected[index]) updateMockStorage(key: quality.name, value: quality_expected[index]) Defaults.iCloud.syncWithoutWaiting(name, quality, source: .remote) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(Defaults[name], name_expected[index]) XCTAssertEqual(Defaults[quality], quality_expected[index]) } @@ -237,14 +237,14 @@ final class DefaultsICloudTests: XCTestCase { Defaults[name] = "8" Defaults[quality] = 8.0 Defaults.iCloud.syncWithoutWaiting(name, quality, source: .local) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "8") XCTAssertEqual(mockStorage.data(forKey: quality.name), 8.0) Defaults[name] = nil Defaults[quality] = nil Defaults.iCloud.syncWithoutWaiting(name, quality, source: .local) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertNil(mockStorage.object(forKey: name.name)) XCTAssertNil(mockStorage.object(forKey: quality.name)) } @@ -254,12 +254,12 @@ final class DefaultsICloudTests: XCTestCase { let task = Task.detached { Defaults.iCloud.add(name) Defaults.iCloud.syncWithoutWaiting() - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() } await task.value XCTAssertEqual(mockStorage.data(forKey: name.name), "0") Defaults[name] = "1" - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "1") } @@ -267,7 +267,7 @@ final class DefaultsICloudTests: XCTestCase { let task = Task.detached { let name = Defaults.Key("testICloudInitializeFromDetached_name", default: "0", iCloud: true) - await Defaults.iCloud.sync() + await Defaults.iCloud.waitForSyncCompletion() XCTAssertEqual(mockStorage.data(forKey: name.name), "0") } await task.value diff --git a/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift b/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift index 0067b8c..6acf599 100644 --- a/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift +++ b/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift @@ -350,8 +350,8 @@ final class DefaultsNSSecureCodingTests: XCTestCase { } } - inputArray.forEach { - Defaults[key] = ExamplePersistentHistory(value: $0) + for item in inputArray { + Defaults[key] = ExamplePersistentHistory(value: item) } Defaults.reset(key) @@ -383,8 +383,8 @@ final class DefaultsNSSecureCodingTests: XCTestCase { } } - inputArray.forEach { - Defaults[key] = ExamplePersistentHistory(value: $0) + for item in inputArray { + Defaults[key] = ExamplePersistentHistory(value: item) } Defaults.reset(key) diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index e158e35..b783917 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -593,8 +593,8 @@ final class DefaultsTests: XCTestCase { } } - inputArray.forEach { - Defaults[key] = $0 + for item in inputArray { + Defaults[key] = item } Defaults.reset(key) @@ -625,8 +625,8 @@ final class DefaultsTests: XCTestCase { } } - inputArray.forEach { - Defaults[key] = $0 + for item in inputArray { + Defaults[key] = item } Defaults.reset(key) diff --git a/readme.md b/readme.md index 3b463ff..42ed01f 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ It's used in production by [all my apps](https://sindresorhus.com/apps) (1 milli - macOS 11+ - iOS 14+ - tvOS 14+ -- watchOS 7+ +- watchOS 9+ - visionOS 1+ ## Install