Various tweaks

This commit is contained in:
Sindre Sorhus 2024-03-29 01:45:39 +09:00
parent e800493235
commit 17fddec4d9
14 changed files with 404 additions and 390 deletions

View File

@ -4,10 +4,10 @@ on:
- pull_request - pull_request
jobs: jobs:
test: test:
runs-on: macos-13 runs-on: macos-14
steps: steps:
- uses: actions/checkout@v4 - 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 - run: swift test
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.9 // swift-tools-version:5.10
import PackageDescription import PackageDescription
let package = Package( let package = Package(

View File

@ -171,7 +171,7 @@ extension UserDefaults: DefaultsKeyValueStore {}
extension DefaultsLockProtocol { extension DefaultsLockProtocol {
@discardableResult @discardableResult
func with<R>(_ body: @Sendable () throws -> R) rethrows -> R where R: Sendable { func with<R>(_ body: @Sendable () throws -> R) rethrows -> R where R: Sendable {
self.lock() lock()
defer { defer {
self.unlock() self.unlock()
} }

View File

@ -1,6 +1,23 @@
import Foundation import Foundation
public protocol _DefaultsSerializable { extension Defaults {
/**
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 protocol Serializable {
typealias Value = Bridge.Value typealias Value = Bridge.Value
typealias Serializable = Bridge.Serializable typealias Serializable = Bridge.Serializable
associatedtype Bridge: Defaults.Bridge associatedtype Bridge: Defaults.Bridge
@ -15,36 +32,59 @@ public protocol _DefaultsSerializable {
*/ */
static var isNativelySupportedType: Bool { get } static var isNativelySupportedType: Bool { get }
} }
}
public protocol _DefaultsBridge { extension Defaults {
public protocol Bridge {
associatedtype Value associatedtype Value
associatedtype Serializable associatedtype Serializable
func serialize(_ value: Value?) -> Serializable? func serialize(_ value: Value?) -> Serializable?
func deserialize(_ object: Serializable?) -> Value? func deserialize(_ object: Serializable?) -> Value?
} }
}
public protocol _DefaultsCollectionSerializable: Collection, Defaults.Serializable { extension Defaults {
/**
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 {}
/**
Ambiguous bridge selector protocol that lets you select your preferred bridge when there are multiple possibilities.
*/
public protocol PreferNSSecureCoding: NSObject, NSSecureCoding {}
}
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`. `Collection` does not have a initializer, but we need a initializer to convert an array into the `Value`.
*/ */
init(_ elements: [Element]) init(_ elements: [Element])
} }
public protocol _DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable { public protocol SetAlgebraSerializable: SetAlgebra, Serializable {
/** /**
Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly. Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly.
*/ */
func toArray() -> [Element] func toArray() -> [Element]
} }
public protocol _DefaultsCodableBridge: Defaults.Bridge where Serializable == String, Value: Codable {} public protocol CodableBridge: Bridge where Serializable == String, Value: Codable {}
public protocol _DefaultsPreferRawRepresentable: RawRepresentable {}
public protocol _DefaultsPreferNSSecureCoding: NSObject, NSSecureCoding {}
// Essential properties for serializing and deserializing `ClosedRange` and `Range`. // Essential properties for serializing and deserializing `ClosedRange` and `Range`.
public protocol _DefaultsRange { public protocol Range {
associatedtype Bound: Comparable, Defaults.Serializable associatedtype Bound: Comparable, Defaults.Serializable
var lowerBound: Bound { get } var lowerBound: Bound { get }
@ -53,6 +93,61 @@ public protocol _DefaultsRange {
init(uncheckedBounds: (lower: Bound, upper: Bound)) 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
}
/** /**
Essential properties for synchronizing a key value store. Essential properties for synchronizing a key value store.
*/ */

View File

@ -6,6 +6,196 @@ import UIKit
#endif #endif
import Combine import Combine
import Foundation 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<Bool>("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<Bool>("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<Defaults.Keys> { 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<Bool>("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 { private enum SyncStatus {
case idle case idle
@ -31,7 +221,7 @@ final class iCloudSynchronizer {
@TaskLocal static var timestamp: Date? @TaskLocal static var timestamp: Date?
private var cancellables: Set<AnyCancellable> = [] private var cancellables = Set<AnyCancellable>()
/** /**
Key for recording the synchronization between `NSUbiquitousKeyValueStore` and `UserDefaults`. Key for recording the synchronization between `NSUbiquitousKeyValueStore` and `UserDefaults`.
@ -63,14 +253,14 @@ final class iCloudSynchronizer {
guard guard
let self, let self,
let suite = observable.suite, 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. // Prevent triggering local observation when syncing from remote.
!self.remoteSyncingKeys.contains(key) !remoteSyncingKeys.contains(key)
else { else {
return return
} }
self.enqueue { enqueue {
self.recordTimestamp(forKey: key, timestamp: Self.timestamp, source: .local) self.recordTimestamp(forKey: key, timestamp: Self.timestamp, source: .local)
await self.syncKey(key, source: .local) await self.syncKey(key, source: .local)
} }
@ -81,7 +271,7 @@ final class iCloudSynchronizer {
*/ */
func add(_ keys: [Defaults.Keys]) { func add(_ keys: [Defaults.Keys]) {
self.keys.formUnion(keys) self.keys.formUnion(keys)
self.syncWithoutWaiting(keys) syncWithoutWaiting(keys)
for key in keys { for key in keys {
localKeysMonitor.addObserver(key) 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 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). - 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 let keys = keys.isEmpty ? Array(self.keys) : keys
for key in keys { for key in keys {
let latest = source ?? latestDataSource(forKey: key) let latest = source ?? latestDataSource(forKey: key)
self.enqueue { enqueue {
await self.syncKey(key, source: latest) await self.syncKey(key, source: latest)
} }
} }
@ -141,7 +331,7 @@ final class iCloudSynchronizer {
Enqueue the synchronization task into `backgroundQueue` with the current timestamp. Enqueue the synchronization task into `backgroundQueue` with the current timestamp.
*/ */
private func enqueue(_ task: @escaping TaskQueue.AsyncTask) { private func enqueue(_ task: @escaping TaskQueue.AsyncTask) {
self.backgroundQueue.async { backgroundQueue.async {
await Self.$timestamp.withValue(Date()) { await Self.$timestamp.withValue(Date()) {
await task() await task()
} }
@ -154,7 +344,7 @@ final class iCloudSynchronizer {
- Parameter key: The key to synchronize. - Parameter key: The key to synchronize.
- Parameter source: Sync key from which data source (remote or local). - 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) Self.logKeySyncStatus(key, source: source, syncStatus: .idle)
switch source { switch source {
@ -227,7 +417,7 @@ final class iCloudSynchronizer {
The timestamp storage format varies across different source providers due to storage limitations. 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 { switch source {
case .remote: case .remote:
guard guard
@ -252,7 +442,7 @@ final class iCloudSynchronizer {
/** /**
Mark the current timestamp to the given storage. 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 { switch source {
case .remote: case .remote:
guard 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. 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. // 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 return .local
} }
guard let localTimestamp = self.timestamp(forKey: key, source: .local) else { guard let localTimestamp = timestamp(forKey: key, source: .local) else {
return .remote return .remote
} }
@ -288,9 +478,7 @@ final class iCloudSynchronizer {
} }
} }
/** // Notification related functions.
`iCloudSynchronizer` notification related functions.
*/
extension iCloudSynchronizer { extension iCloudSynchronizer {
private func registerNotifications() { private func registerNotifications() {
// TODO: Replace it with async stream when Swift supports custom executors. // TODO: Replace it with async stream when Swift supports custom executors.
@ -301,27 +489,28 @@ extension iCloudSynchronizer {
return return
} }
self.didChangeExternally(notification: notification) didChangeExternally(notification: notification)
} }
.store(in: &cancellables) .store(in: &cancellables)
// TODO: Replace it with async stream when Swift supports custom executors. #if canImport(UIKit)
#if os(iOS) || os(tvOS) || os(visionOS) #if os(watchOS)
NotificationCenter.default let notificationName = WKExtension.applicationWillEnterForegroundNotification
.publisher(for: UIScene.willEnterForegroundNotification) #else
#elseif os(watchOS) let notificationName = UIScene.willEnterForegroundNotification
NotificationCenter.default
.publisher(for: WKExtension.applicationWillEnterForegroundNotification)
#endif #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 .sink { [weak self] notification in
guard let self else { guard let self else {
return return
} }
self.willEnterForeground(notification: notification) willEnterForeground(notification: notification)
} }
.store(in: cancellables) .store(in: &cancellables)
#endif #endif
} }
@ -343,7 +532,7 @@ extension iCloudSynchronizer {
return 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 { guard let remoteTimestamp = self.timestamp(forKey: key, source: .remote) else {
continue continue
} }
@ -361,14 +550,16 @@ extension iCloudSynchronizer {
} }
} }
/** // Logging related functions.
`iCloudSynchronizer` logging related functions.
*/
extension iCloudSynchronizer { extension iCloudSynchronizer {
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, visionOS 1.0, *)
private static let logger = Logger(OSLog.default) 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 { guard Defaults.iCloud.isDebug else {
return return
} }
@ -401,173 +592,6 @@ extension iCloudSynchronizer {
return return
} }
if #available(macOS 11, iOS 14, tvOS 14, watchOS 7, visionOS 1.0, *) {
logger.debug("[Defaults.iCloud] \(message)") 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<Bool>("isUnicornMode", default: true, iCloud: true)
}
Task {
let quality = Defaults.Key<Int>("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<Defaults.Keys> { 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)
}
} }
} }

View File

@ -67,6 +67,8 @@ extension Defaults {
suite.removeObject(forKey: name) suite.removeObject(forKey: name)
} }
} }
public typealias Keys = _AnyKey
} }
extension Defaults { 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<Value: Serializable>: _AnyKey { public final class Key<Value: Serializable>: _AnyKey {
/** /**
@ -103,7 +105,7 @@ extension Defaults {
Create a key. Create a key.
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`). - 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. 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 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. - 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. Create a key with an optional value.
- Parameter name: The name must be ASCII, not start with `@`, and cannot contain a dot (`.`). - 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<T>( public convenience init<T>(
_ name: String, _ 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 { extension Defaults {
/** /**
Observe updates to a stored value. 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( public static func updates(
_ keys: [_AnyKey], _ keys: [_AnyKey],

View File

@ -51,7 +51,7 @@ typealias Default = _Default
### Methods ### Methods
- ``Defaults/updates(_:initial:)-9eh8`` - ``Defaults/updates(_:initial:)-88orv``
- ``Defaults/updates(_:initial:)-1mqkb`` - ``Defaults/updates(_:initial:)-1mqkb``
- ``Defaults/reset(_:)-7jv5v`` - ``Defaults/reset(_:)-7jv5v``
- ``Defaults/reset(_:)-7es1e`` - ``Defaults/reset(_:)-7es1e``

View File

@ -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<Value: Serializable>( public static func publisher<Value: Serializable>(
_ key: Key<Value>, _ key: Key<Value>,
@ -99,7 +99,7 @@ extension Defaults {
/** /**
Publisher for multiple `Key<T>` observation, but without specific information about changes. Publisher for multiple `Key<T>` 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( public static func publisher(
keys: [_AnyKey], keys: [_AnyKey],
@ -122,7 +122,7 @@ extension Defaults {
/** /**
Publisher for multiple `Key<T>` observation, but without specific information about changes. Publisher for multiple `Key<T>` 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( public static func publisher(
keys: _AnyKey..., keys: _AnyKey...,

View File

@ -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<Value: Serializable>( public static func observe<Value: Serializable>(
_ key: Key<Value>, _ key: Key<Value>,
@ -407,7 +407,6 @@ extension Defaults {
return observation return observation
} }
/** /**
Observe multiple keys of any type, but without any information about the changes. 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( public static func observe(
keys: _AnyKey..., keys: _AnyKey...,

View File

@ -235,13 +235,14 @@ extension Defaults.Serializable {
} }
} }
// TODO: Remove this in favor of `MutexLock` when targeting Swift 6.
// swiftlint:disable:next final_class // swiftlint:disable:next final_class
class Lock: DefaultsLockProtocol { class Lock: DefaultsLockProtocol {
final class UnfairLock: Lock { final class UnfairLock: Lock {
private let _lock: os_unfair_lock_t private let _lock: os_unfair_lock_t
override init() { override init() {
_lock = .allocate(capacity: 1) self._lock = .allocate(capacity: 1)
_lock.initialize(to: os_unfair_lock()) _lock.initialize(to: os_unfair_lock())
} }
@ -360,13 +361,12 @@ final class TaskQueue {
queueContinuation?.yield { queueContinuation?.yield {
continuation.resume() 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 @propertyWrapper
final class Atomic<Value> { final class Atomic<Value> {
private let lock: Lock = .make() private let lock: Lock = .make()

View File

@ -83,7 +83,7 @@ final class DefaultsICloudTests: XCTestCase {
Defaults.removeAll() Defaults.removeAll()
} }
private func updateMockStorage<T>(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) mockStorage.set([date ?? Date(), value], forKey: key)
} }
@ -93,7 +93,7 @@ final class DefaultsICloudTests: XCTestCase {
let quality = Defaults.Key<Double>("testICloudInitialize_quality", default: 0.0, iCloud: true) let quality = Defaults.Key<Double>("testICloudInitialize_quality", default: 0.0, iCloud: true)
print(Defaults.iCloud.keys) print(Defaults.iCloud.keys)
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "0") XCTAssertEqual(mockStorage.data(forKey: name.name), "0")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 0.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 0.0)
let name_expected = ["1", "2", "3", "4", "5", "6", "7"] let name_expected = ["1", "2", "3", "4", "5", "6", "7"]
@ -102,7 +102,7 @@ final class DefaultsICloudTests: XCTestCase {
for index in 0..<name_expected.count { for index in 0..<name_expected.count {
Defaults[name] = name_expected[index] Defaults[name] = name_expected[index]
Defaults[quality] = quality_expected[index] Defaults[quality] = quality_expected[index]
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), name_expected[index]) XCTAssertEqual(mockStorage.data(forKey: name.name), name_expected[index])
XCTAssertEqual(mockStorage.data(forKey: quality.name), quality_expected[index]) XCTAssertEqual(mockStorage.data(forKey: quality.name), quality_expected[index])
} }
@ -110,20 +110,20 @@ final class DefaultsICloudTests: XCTestCase {
updateMockStorage(key: quality.name, value: 8.0) updateMockStorage(key: quality.name, value: 8.0)
updateMockStorage(key: name.name, value: "8") updateMockStorage(key: name.name, value: "8")
mockStorage.synchronize() mockStorage.synchronize()
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[quality], 8.0) XCTAssertEqual(Defaults[quality], 8.0)
XCTAssertEqual(Defaults[name], "8") XCTAssertEqual(Defaults[name], "8")
Defaults[name] = "9" Defaults[name] = "9"
Defaults[quality] = 9.0 Defaults[quality] = 9.0
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "9") XCTAssertEqual(mockStorage.data(forKey: name.name), "9")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 9.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 9.0)
updateMockStorage(key: quality.name, value: 10) updateMockStorage(key: quality.name, value: 10)
updateMockStorage(key: name.name, value: "10") updateMockStorage(key: name.name, value: "10")
mockStorage.synchronize() mockStorage.synchronize()
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[quality], 10.0) XCTAssertEqual(Defaults[quality], 10.0)
XCTAssertEqual(Defaults[name], "10") XCTAssertEqual(Defaults[name], "10")
} }
@ -133,7 +133,7 @@ final class DefaultsICloudTests: XCTestCase {
updateMockStorage(key: "testDidChangeExternallyNotification_quality", value: 0.0) updateMockStorage(key: "testDidChangeExternallyNotification_quality", value: 0.0)
let name = Defaults.Key<String?>("testDidChangeExternallyNotification_name", iCloud: true) let name = Defaults.Key<String?>("testDidChangeExternallyNotification_name", iCloud: true)
let quality = Defaults.Key<Double?>("testDidChangeExternallyNotification_quality", iCloud: true) let quality = Defaults.Key<Double?>("testDidChangeExternallyNotification_quality", iCloud: true)
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[name], "0") XCTAssertEqual(Defaults[name], "0")
XCTAssertEqual(Defaults[quality], 0.0) XCTAssertEqual(Defaults[quality], 0.0)
let name_expected = ["1", "2", "3", "4", "5", "6", "7"] 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]) updateMockStorage(key: quality.name, value: quality_expected[index])
mockStorage.synchronize() mockStorage.synchronize()
} }
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[name], "7") XCTAssertEqual(Defaults[name], "7")
XCTAssertEqual(Defaults[quality], 7.0) XCTAssertEqual(Defaults[quality], 7.0)
Defaults[name] = "8" Defaults[name] = "8"
Defaults[quality] = 8.0 Defaults[quality] = 8.0
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "8") XCTAssertEqual(mockStorage.data(forKey: name.name), "8")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 8.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 8.0)
Defaults[name] = nil Defaults[name] = nil
Defaults[quality] = nil Defaults[quality] = nil
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertNil(mockStorage.data(forKey: name.name)) XCTAssertNil(mockStorage.data(forKey: name.name))
XCTAssertNil(mockStorage.data(forKey: quality.name)) XCTAssertNil(mockStorage.data(forKey: quality.name))
} }
@ -174,7 +174,7 @@ final class DefaultsICloudTests: XCTestCase {
XCTAssertEqual(Defaults[quality], quality_expected[index]) 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: name.name), "7")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 7.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 7.0)
} }
@ -184,14 +184,14 @@ final class DefaultsICloudTests: XCTestCase {
let quality = Defaults.Key<Double>("testRemoveKey_quality", default: 0.0, iCloud: true) let quality = Defaults.Key<Double>("testRemoveKey_quality", default: 0.0, iCloud: true)
Defaults[name] = "1" Defaults[name] = "1"
Defaults[quality] = 1.0 Defaults[quality] = 1.0
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "1") XCTAssertEqual(mockStorage.data(forKey: name.name), "1")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 1.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 1.0)
Defaults.iCloud.remove(quality) Defaults.iCloud.remove(quality)
Defaults[name] = "2" Defaults[name] = "2"
Defaults[quality] = 1.0 Defaults[quality] = 1.0
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "2") XCTAssertEqual(mockStorage.data(forKey: name.name), "2")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 1.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 1.0)
} }
@ -206,7 +206,7 @@ final class DefaultsICloudTests: XCTestCase {
Defaults[name] = name_expected[index] Defaults[name] = name_expected[index]
Defaults[quality] = quality_expected[index] Defaults[quality] = quality_expected[index]
Defaults.iCloud.syncWithoutWaiting(name, quality, source: .local) 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: name.name), name_expected[index])
XCTAssertEqual(mockStorage.data(forKey: quality.name), quality_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: name.name, value: "8")
updateMockStorage(key: quality.name, value: 8) updateMockStorage(key: quality.name, value: 8)
Defaults.iCloud.syncWithoutWaiting(name, quality, source: .remote) Defaults.iCloud.syncWithoutWaiting(name, quality, source: .remote)
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[quality], 8.0) XCTAssertEqual(Defaults[quality], 8.0)
XCTAssertEqual(Defaults[name], "8") XCTAssertEqual(Defaults[name], "8")
} }
@ -229,7 +229,7 @@ final class DefaultsICloudTests: XCTestCase {
updateMockStorage(key: name.name, value: name_expected[index]) updateMockStorage(key: name.name, value: name_expected[index])
updateMockStorage(key: quality.name, value: quality_expected[index]) updateMockStorage(key: quality.name, value: quality_expected[index])
Defaults.iCloud.syncWithoutWaiting(name, quality, source: .remote) Defaults.iCloud.syncWithoutWaiting(name, quality, source: .remote)
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[name], name_expected[index]) XCTAssertEqual(Defaults[name], name_expected[index])
XCTAssertEqual(Defaults[quality], quality_expected[index]) XCTAssertEqual(Defaults[quality], quality_expected[index])
} }
@ -237,14 +237,14 @@ final class DefaultsICloudTests: XCTestCase {
Defaults[name] = "8" Defaults[name] = "8"
Defaults[quality] = 8.0 Defaults[quality] = 8.0
Defaults.iCloud.syncWithoutWaiting(name, quality, source: .local) 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: name.name), "8")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 8.0) XCTAssertEqual(mockStorage.data(forKey: quality.name), 8.0)
Defaults[name] = nil Defaults[name] = nil
Defaults[quality] = nil Defaults[quality] = nil
Defaults.iCloud.syncWithoutWaiting(name, quality, source: .local) 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: name.name))
XCTAssertNil(mockStorage.object(forKey: quality.name)) XCTAssertNil(mockStorage.object(forKey: quality.name))
} }
@ -254,12 +254,12 @@ final class DefaultsICloudTests: XCTestCase {
let task = Task.detached { let task = Task.detached {
Defaults.iCloud.add(name) Defaults.iCloud.add(name)
Defaults.iCloud.syncWithoutWaiting() Defaults.iCloud.syncWithoutWaiting()
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
} }
await task.value await task.value
XCTAssertEqual(mockStorage.data(forKey: name.name), "0") XCTAssertEqual(mockStorage.data(forKey: name.name), "0")
Defaults[name] = "1" Defaults[name] = "1"
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "1") XCTAssertEqual(mockStorage.data(forKey: name.name), "1")
} }
@ -267,7 +267,7 @@ final class DefaultsICloudTests: XCTestCase {
let task = Task.detached { let task = Task.detached {
let name = Defaults.Key<String>("testICloudInitializeFromDetached_name", default: "0", iCloud: true) let name = Defaults.Key<String>("testICloudInitializeFromDetached_name", default: "0", iCloud: true)
await Defaults.iCloud.sync() await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "0") XCTAssertEqual(mockStorage.data(forKey: name.name), "0")
} }
await task.value await task.value

View File

@ -350,8 +350,8 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
} }
} }
inputArray.forEach { for item in inputArray {
Defaults[key] = ExamplePersistentHistory(value: $0) Defaults[key] = ExamplePersistentHistory(value: item)
} }
Defaults.reset(key) Defaults.reset(key)
@ -383,8 +383,8 @@ final class DefaultsNSSecureCodingTests: XCTestCase {
} }
} }
inputArray.forEach { for item in inputArray {
Defaults[key] = ExamplePersistentHistory(value: $0) Defaults[key] = ExamplePersistentHistory(value: item)
} }
Defaults.reset(key) Defaults.reset(key)

View File

@ -593,8 +593,8 @@ final class DefaultsTests: XCTestCase {
} }
} }
inputArray.forEach { for item in inputArray {
Defaults[key] = $0 Defaults[key] = item
} }
Defaults.reset(key) Defaults.reset(key)
@ -625,8 +625,8 @@ final class DefaultsTests: XCTestCase {
} }
} }
inputArray.forEach { for item in inputArray {
Defaults[key] = $0 Defaults[key] = item
} }
Defaults.reset(key) Defaults.reset(key)

View File

@ -34,7 +34,7 @@ It's used in production by [all my apps](https://sindresorhus.com/apps) (1 milli
- macOS 11+ - macOS 11+
- iOS 14+ - iOS 14+
- tvOS 14+ - tvOS 14+
- watchOS 7+ - watchOS 9+
- visionOS 1+ - visionOS 1+
## Install ## Install