Skip to content

Commit 034b159

Browse files
Fix handling of initial sync values (#209)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 564b725 commit 034b159

File tree

2 files changed

+65
-12
lines changed

2 files changed

+65
-12
lines changed

Sources/Defaults/Defaults+iCloud.swift

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ private enum SyncStatus {
196196
case idle
197197
case syncing
198198
case completed
199+
case aborted
199200
}
200201

201202
/**
@@ -326,12 +327,18 @@ final class iCloudSynchronizer {
326327

327328
- Parameter keys: If the keys parameter is an empty array, the method will use the keys that were added to `Defaults.iCloud`.
328329
- Parameter source: Sync keys from which data source (remote or local).
330+
331+
- Note: The synchronization task might be aborted if both the remote and local data sources do not exist.
329332
*/
330333
func syncWithoutWaiting(_ keys: [Defaults.Keys] = [], _ source: Defaults.iCloud.DataSource? = nil) {
331334
let keys = keys.isEmpty ? Array(self.keys) : keys
332335

333336
for key in keys {
334-
let latest = source ?? latestDataSource(forKey: key)
337+
// If no data source is specified, we should abort this synchronization task.
338+
guard let latest = source ?? latestDataSource(forKey: key) else {
339+
Self.logKeySyncStatus(key, source: nil, syncStatus: .aborted, value: nil)
340+
continue
341+
}
335342
enqueue {
336343
await self.syncKey(key, source: latest)
337344
}
@@ -483,16 +490,22 @@ final class iCloudSynchronizer {
483490
/**
484491
Determine which data source has the latest data available by comparing the timestamps of the local and remote sources.
485492
*/
486-
private func latestDataSource(forKey key: Defaults.Keys) -> Defaults.iCloud.DataSource {
493+
private func latestDataSource(forKey key: Defaults.Keys) -> Defaults.iCloud.DataSource? {
494+
let remoteTimestamp = timestamp(forKey: key, source: .remote)
495+
let localTimestamp = timestamp(forKey: key, source: .local)
496+
return switch (remoteTimestamp, localTimestamp) {
497+
// If the local timestamp does not exist, use the remote timestamp as the latest data source.
498+
case (.some(_), nil):
499+
.remote
487500
// If the remote timestamp does not exist, use the local timestamp as the latest data source.
488-
guard let remoteTimestamp = timestamp(forKey: key, source: .remote) else {
489-
return .local
501+
case (nil, .some(_)):
502+
.local
503+
case let (.some(remoteTimestamp), .some(localTimestamp)):
504+
localTimestamp > remoteTimestamp ? .local : .remote
505+
// If both remote and local timestamp does not exist, return nil
506+
case (nil, nil):
507+
nil
490508
}
491-
guard let localTimestamp = timestamp(forKey: key, source: .local) else {
492-
return .remote
493-
}
494-
495-
return localTimestamp > remoteTimestamp ? .local : .remote
496509
}
497510
}
498511

@@ -575,7 +588,7 @@ extension iCloudSynchronizer {
575588

576589
private static func logKeySyncStatus(
577590
_ key: Defaults.Keys,
578-
source: Defaults.iCloud.DataSource,
591+
source: Defaults.iCloud.DataSource?,
579592
syncStatus: SyncStatus,
580593
value: Any? = nil
581594
) {
@@ -588,6 +601,8 @@ extension iCloudSynchronizer {
588601
"from local"
589602
case .remote:
590603
"from remote"
604+
case .none:
605+
""
591606
}
592607

593608
let status: String
@@ -600,6 +615,8 @@ extension iCloudSynchronizer {
600615
valueDescription = " with value \(value ?? "nil") "
601616
case .completed:
602617
status = "Complete synchronization"
618+
case .aborted:
619+
status = "Aborting"
603620
}
604621

605622
let message = "\(status) key '\(key.name)'\(valueDescription)\(destination)"

Tests/DefaultsTests/Defaults+iCloudTests.swift

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,48 @@ final class DefaultsICloudTests {
245245
#expect(mockStorage.object(forKey: quality.name) == nil)
246246
}
247247

248+
@Test
249+
func testAbortion() async {
250+
let name = Defaults.Key<String>("testAbortSignleKey_name", default: "0", iCloud: true) // swiftlint:disable:this discouraged_optional_boolean
251+
let quantity = Defaults.Key<Int>("testAbortSignleKey_quantity", default: 0, iCloud: true) // swiftlint:disable:this discouraged_optional_boolean
252+
Defaults[quantity] = 1
253+
await Defaults.iCloud.waitForSyncCompletion()
254+
#expect(mockStorage.data(forKey: quantity.name) == 1)
255+
updateMockStorage(key: quantity.name, value: 2)
256+
Defaults.iCloud.syncWithoutWaiting()
257+
await Defaults.iCloud.waitForSyncCompletion()
258+
#expect(Defaults[name] == "0")
259+
#expect(Defaults[quantity] == 2)
260+
}
261+
262+
263+
@Test
264+
func testSyncLatestSource() async {
265+
let name = Defaults.Key<String>("testSyncLatestSource_name", default: "0", iCloud: true) // swiftlint:disable:this discouraged_optional_boolean
266+
let quantity = Defaults.Key<Int>("testSyncLatestSource_quantity", default: 0, iCloud: true) // swiftlint:disable:this discouraged_optional_boolean
267+
// Create a timestamp in both the local and remote data sources
268+
Defaults[name] = "1"
269+
Defaults[quantity] = 1
270+
await Defaults.iCloud.waitForSyncCompletion()
271+
#expect(mockStorage.data(forKey: name.name) == "1")
272+
#expect(mockStorage.data(forKey: quantity.name) == 1)
273+
// Update remote storage
274+
updateMockStorage(key: name.name, value: "2")
275+
updateMockStorage(key: quantity.name, value: 2)
276+
Defaults.iCloud.syncWithoutWaiting()
277+
await Defaults.iCloud.waitForSyncCompletion()
278+
#expect(Defaults[name] == "2")
279+
#expect(Defaults[quantity] == 2)
280+
}
281+
248282
@Test
249283
func testAddFromDetached() async {
250-
let name = Defaults.Key<String>("testInitAddFromDetached_name", default: "0", suite: suite)
251-
let quantity = Defaults.Key<Bool>("testInitAddFromDetached_quantity", default: false, suite: suite)
284+
let name = Defaults.Key<String?>("testInitAddFromDetached_name", suite: suite) // swiftlint:disable:this discouraged_optional_boolean
285+
let quantity = Defaults.Key<Bool?>("testInitAddFromDetached_quantity", suite: suite) // swiftlint:disable:this discouraged_optional_boolean
252286
await Task.detached {
253287
Defaults.iCloud.add(name, quantity)
288+
Defaults[name] = "0"
289+
Defaults[quantity] = true
254290
Defaults.iCloud.syncWithoutWaiting()
255291
await Defaults.iCloud.waitForSyncCompletion()
256292
}.value

0 commit comments

Comments
 (0)