Various tweaks
This commit is contained in:
parent
e800493235
commit
17fddec4d9
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// swift-tools-version:5.9
|
||||
// swift-tools-version:5.10
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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``
|
||||
|
|
|
@ -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...,
|
||||
|
|
|
@ -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...,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue