Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions Sources/Defaults/Defaults+iCloud.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ private enum SyncStatus {
case idle
case syncing
case completed
case aborted
}

/**
Expand Down Expand Up @@ -326,12 +327,18 @@ 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).

- Note: The synchronization task might be aborted if both the remote and local data sources do not exist.
*/
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)
// If no data source is specified, we should abort this synchronization task.
guard let latest = source ?? latestDataSource(forKey: key) else {
Self.logKeySyncStatus(key, source: nil, syncStatus: .aborted, value: nil)
continue
}
enqueue {
await self.syncKey(key, source: latest)
}
Expand Down Expand Up @@ -483,16 +490,22 @@ 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.iCloud.DataSource {
private func latestDataSource(forKey key: Defaults.Keys) -> Defaults.iCloud.DataSource? {
let remoteTimestamp = timestamp(forKey: key, source: .remote)
let localTimestamp = timestamp(forKey: key, source: .local)
return switch (remoteTimestamp, localTimestamp) {
// If the local timestamp does not exist, use the remote timestamp as the latest data source.
case (.some(_), nil):
.remote
// If the remote timestamp does not exist, use the local timestamp as the latest data source.
guard let remoteTimestamp = timestamp(forKey: key, source: .remote) else {
return .local
case (nil, .some(_)):
.local
case let (.some(remoteTimestamp), .some(localTimestamp)):
localTimestamp > remoteTimestamp ? .local : .remote
// If both remote and local timestamp does not exist, return nil
case (nil, nil):
nil
}
guard let localTimestamp = timestamp(forKey: key, source: .local) else {
return .remote
}

return localTimestamp > remoteTimestamp ? .local : .remote
}
}

Expand Down Expand Up @@ -575,7 +588,7 @@ extension iCloudSynchronizer {

private static func logKeySyncStatus(
_ key: Defaults.Keys,
source: Defaults.iCloud.DataSource,
source: Defaults.iCloud.DataSource?,
syncStatus: SyncStatus,
value: Any? = nil
) {
Expand All @@ -588,6 +601,8 @@ extension iCloudSynchronizer {
"from local"
case .remote:
"from remote"
case .none:
""
}

let status: String
Expand All @@ -600,6 +615,8 @@ extension iCloudSynchronizer {
valueDescription = " with value \(value ?? "nil") "
case .completed:
status = "Complete synchronization"
case .aborted:
status = "Aborting"
}

let message = "\(status) key '\(key.name)'\(valueDescription)\(destination)"
Expand Down
40 changes: 38 additions & 2 deletions Tests/DefaultsTests/Defaults+iCloudTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,48 @@
#expect(mockStorage.object(forKey: quality.name) == nil)
}

@Test
func testAbortion() async {
let name = Defaults.Key<String>("testAbortSignleKey_name", default: "0", iCloud: true) // swiftlint:disable:this discouraged_optional_boolean

Check warning on line 250 in Tests/DefaultsTests/Defaults+iCloudTests.swift

View workflow job for this annotation

GitHub Actions / lint

Superfluous Disable Command Violation: SwiftLint rule 'discouraged_optional_boolean' did not trigger a violation in the disabled region; remove the disable command (superfluous_disable_command)
let quantity = Defaults.Key<Int>("testAbortSignleKey_quantity", default: 0, iCloud: true) // swiftlint:disable:this discouraged_optional_boolean
Defaults[quantity] = 1
await Defaults.iCloud.waitForSyncCompletion()
#expect(mockStorage.data(forKey: quantity.name) == 1)
updateMockStorage(key: quantity.name, value: 2)
Defaults.iCloud.syncWithoutWaiting()
await Defaults.iCloud.waitForSyncCompletion()
#expect(Defaults[name] == "0")
#expect(Defaults[quantity] == 2)
}


@Test
func testSyncLatestSource() async {
let name = Defaults.Key<String>("testSyncLatestSource_name", default: "0", iCloud: true) // swiftlint:disable:this discouraged_optional_boolean

Check warning on line 265 in Tests/DefaultsTests/Defaults+iCloudTests.swift

View workflow job for this annotation

GitHub Actions / lint

Superfluous Disable Command Violation: SwiftLint rule 'discouraged_optional_boolean' did not trigger a violation in the disabled region; remove the disable command (superfluous_disable_command)
let quantity = Defaults.Key<Int>("testSyncLatestSource_quantity", default: 0, iCloud: true) // swiftlint:disable:this discouraged_optional_boolean
// Create a timestamp in both the local and remote data sources
Defaults[name] = "1"
Defaults[quantity] = 1
await Defaults.iCloud.waitForSyncCompletion()
#expect(mockStorage.data(forKey: name.name) == "1")
#expect(mockStorage.data(forKey: quantity.name) == 1)
// Update remote storage
updateMockStorage(key: name.name, value: "2")
updateMockStorage(key: quantity.name, value: 2)
Defaults.iCloud.syncWithoutWaiting()
await Defaults.iCloud.waitForSyncCompletion()
#expect(Defaults[name] == "2")
#expect(Defaults[quantity] == 2)
}

@Test
func testAddFromDetached() async {
let name = Defaults.Key<String>("testInitAddFromDetached_name", default: "0", suite: suite)
let quantity = Defaults.Key<Bool>("testInitAddFromDetached_quantity", default: false, suite: suite)
let name = Defaults.Key<String?>("testInitAddFromDetached_name", suite: suite) // swiftlint:disable:this discouraged_optional_boolean
let quantity = Defaults.Key<Bool?>("testInitAddFromDetached_quantity", suite: suite) // swiftlint:disable:this discouraged_optional_boolean
await Task.detached {
Defaults.iCloud.add(name, quantity)
Defaults[name] = "0"
Defaults[quantity] = true
Defaults.iCloud.syncWithoutWaiting()
await Defaults.iCloud.waitForSyncCompletion()
}.value
Expand Down