Skip to content

Conversation

hank121314
Copy link
Collaborator

@hank121314 hank121314 commented Jul 29, 2025

Description

This PR fixes #206.

Previously, when a user newly installed the app, we would create some synchronization tasks.
Since the app was newly installed, both the remote and local data sources did not have a timestamp record, so we would sync the local data source to the remote one, which might override values in iCloud.

With this PR, we will abort the synchronization task if both the remote and local data sources do not have a timestamp record.
This prevents unintended data overrides and allows developers to wait for iCloud synchronization instead (call synchronize on app launch and wait for didChangeExternallyNotification).

Implementation

  • Make Defaults.iCloud.latestDataSource return an optional. If it returns nil, we should abort the synchronization task.
  • Add a new SyncStatus.abort in the logger to indicate that the key synchronization has been aborted.

Discussion

Should we provide a function that ensures iCloud is synced?

ex.

func waitForRemoteSynced() async -> Notification? {
	await NotificationCenter.default.notifications(named: NSUbiquitousKeyValueStore.didChangeExternallyNotification).first
}

@hank121314 hank121314 requested a review from sindresorhus July 29, 2025 15:28
@sindresorhus
Copy link
Owner

Should we provide a function that ensures iCloud is synced?

No. We cannot reliably guarantee it. NSUbiquitousKeyValueStore.didChangeExternallyNotification may not fire and could cause the method to hang forever. And it would also be to easy to misuse it, like blocking app startup waiting for it.

// If no data source is specified, we should abort the synchronization task.
guard let latest = source ?? latestDataSource(forKey: key) else {
Self.logKeySyncStatus(key, source: .local, syncStatus: .abort, value: nil)
return
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be continue instead? Do we really want to cancel all sync tasks just because the first key does not have a timestamp?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're absolutely right, many thanks for catching this 🙏 .
Also add a test for it.

return .local
case (nil, .some(_)):
.local
case let (.some(localTimestamp), .some(remoteTimestamp)):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inverted.

Make sure to add a test for this case that would have failed.

Copy link
Collaborator Author

@hank121314 hank121314 Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My fault, thought it was already covered by the tests.
Added a test for latestDataSource.
The code coverage for Defaults.iCloud has increased to 90% now.

let latest = source ?? latestDataSource(forKey: key)
// If no data source is specified, we should abort the synchronization task.
guard let latest = source ?? latestDataSource(forKey: key) else {
Self.logKeySyncStatus(key, source: .local, syncStatus: .abort, value: nil)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Self.logKeySyncStatus(key, source: .local, syncStatus: .abort, value: nil)
Self.logKeySyncStatus(key, source: nil, syncStatus: .abort, value: nil)

?

@@ -193,6 +193,7 @@ extension Defaults.iCloud {
}

private enum SyncStatus {
case abort
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case abort
case aborted

?

And should be the last case.

@hank121314 hank121314 force-pushed the fix/abort-nil-dat-source branch 2 times, most recently from 5d5513e to 12bfc4b Compare August 30, 2025 07:11
@hank121314 hank121314 force-pushed the fix/abort-nil-dat-source branch from 12bfc4b to 5fc5dd8 Compare August 30, 2025 07:12
@sindresorhus sindresorhus merged commit 034b159 into sindresorhus:main Aug 30, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

iCloud Settings Sync Issue: New App Installation Overwrites Existing Values with Empty Defaults
2 participants