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
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

View File

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

View File

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

View File

@ -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
}
/**

View File

@ -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<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 {
case idle
@ -31,7 +221,7 @@ final class iCloudSynchronizer {
@TaskLocal static var timestamp: Date?
private var cancellables: Set<AnyCancellable> = []
private var cancellables = Set<AnyCancellable>()
/**
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<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)
}
logger.debug("[Defaults.iCloud] \(message)")
}
}

View File

@ -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<Value: Serializable>: _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<T>(
_ 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],

View File

@ -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``

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>(
_ key: Key<Value>,
@ -99,7 +99,7 @@ extension Defaults {
/**
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(
keys: [_AnyKey],
@ -122,7 +122,7 @@ extension Defaults {
/**
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(
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>(
_ key: Key<Value>,
@ -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...,

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
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<Value> {
private let lock: Lock = .make()

View File

@ -83,7 +83,7 @@ final class DefaultsICloudTests: XCTestCase {
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)
}
@ -93,7 +93,7 @@ final class DefaultsICloudTests: XCTestCase {
let quality = Defaults.Key<Double>("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..<name_expected.count {
Defaults[name] = name_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: quality.name), quality_expected[index])
}
@ -110,20 +110,20 @@ final class DefaultsICloudTests: XCTestCase {
updateMockStorage(key: quality.name, value: 8.0)
updateMockStorage(key: name.name, value: "8")
mockStorage.synchronize()
await Defaults.iCloud.sync()
await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[quality], 8.0)
XCTAssertEqual(Defaults[name], "8")
Defaults[name] = "9"
Defaults[quality] = 9.0
await Defaults.iCloud.sync()
await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "9")
XCTAssertEqual(mockStorage.data(forKey: quality.name), 9.0)
updateMockStorage(key: quality.name, value: 10)
updateMockStorage(key: name.name, value: "10")
mockStorage.synchronize()
await Defaults.iCloud.sync()
await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(Defaults[quality], 10.0)
XCTAssertEqual(Defaults[name], "10")
}
@ -133,7 +133,7 @@ final class DefaultsICloudTests: XCTestCase {
updateMockStorage(key: "testDidChangeExternallyNotification_quality", value: 0.0)
let name = Defaults.Key<String?>("testDidChangeExternallyNotification_name", 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[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<Double>("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<String>("testICloudInitializeFromDetached_name", default: "0", iCloud: true)
await Defaults.iCloud.sync()
await Defaults.iCloud.waitForSyncCompletion()
XCTAssertEqual(mockStorage.data(forKey: name.name), "0")
}
await task.value

View File

@ -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)

View File

@ -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)

View File

@ -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