Various tweaks
This commit is contained in:
parent
e800493235
commit
17fddec4d9
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// swift-tools-version:5.9
|
// swift-tools-version:5.10
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -14,43 +31,121 @@ public protocol _DefaultsSerializable {
|
||||||
A flag to determine whether `Value` can be stored natively or not.
|
A flag to determine whether `Value` can be stored natively or not.
|
||||||
*/
|
*/
|
||||||
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 {}
|
// Essential properties for serializing and deserializing `ClosedRange` and `Range`.
|
||||||
public protocol _DefaultsPreferNSSecureCoding: NSObject, NSSecureCoding {}
|
public protocol Range {
|
||||||
|
|
||||||
// Essential properties for serializing and deserializing `ClosedRange` and `Range`.
|
|
||||||
public protocol _DefaultsRange {
|
|
||||||
associatedtype Bound: Comparable, Defaults.Serializable
|
associatedtype Bound: Comparable, Defaults.Serializable
|
||||||
|
|
||||||
var lowerBound: Bound { get }
|
var lowerBound: Bound { get }
|
||||||
var upperBound: Bound { get }
|
var upperBound: Bound { get }
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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``
|
||||||
|
|
|
@ -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...,
|
||||||
|
|
|
@ -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...,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue