Skip to content

fix(ios): stop silent connection loss after TestFlight update#1260

Merged
datlechin merged 1 commit into
mainfrom
fix/ios-sync-data-loss-on-update
May 14, 2026
Merged

fix(ios): stop silent connection loss after TestFlight update#1260
datlechin merged 1 commit into
mainfrom
fix/ios-sync-data-loss-on-update

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

After updating from TestFlight build 12 to 13, some users saw every connection, group, and tag disappear on iPhone while Mac kept everything. CloudKit was untouched. Root cause was a chain of silent failures in the mobile persistence + sync code; this PR closes every link in that chain plus adds a user-facing recovery action.

Bug chain

  1. ConnectionPersistence.load() swallowed every read/decode error with try? and returned []. "File not yet created" and "file unreadable" were indistinguishable.
  2. Files were written with .completeFileProtection (NSFileProtectionComplete), which makes them unreadable while the device is locked. BGAppRefreshTask can fire in that state.
  3. When local = [] and the CloudKit pull returned no new changes since the last token, the sync coordinator merged into [] and unconditionally called onConnectionsChanged([]), which overwrote connections.json with an empty array. The cloud copy stayed intact, but mobile could never recover because pull is incremental.

Fixes (defense in depth)

  • ConnectionPersistence, GroupPersistence, TagPersistenceload() throws. File-not-found returns the empty/preset baseline; any other failure is propagated and logged via OSLog.
  • File protection lowered to .completeFileProtectionUntilFirstUserAuthentication, the level Apple recommends for app data that needs to survive background access (used by Mail, Notes).
  • AppState.persistenceIntegrity tracks load state (.ok / .loadFailed). retryLoadIfFailed() re-attempts the load on each .active scene transition and before background sync runs. Sync is skipped while integrity is uncertain.
  • IOSSyncCoordinator only fires onConnectionsChanged / onGroupsChanged / onTagsChanged when the pull returned actual changes or deletions. No-op pulls no longer overwrite the on-disk file.
  • New resetSyncToken() clears the local server change token and cached CKRecords, then runs a full pull. Surfaced in Settings > Sync as Refresh from iCloud with a native confirmation dialog. Settings > Sync also gained a Last Sync row and a Sync Now button.

Test plan

  • Cold-launch with existing connections — list loads as before, no extra sync churn.
  • Settings > Sync shows correct Last Sync time, updates after Sync Now.
  • Disable Wi-Fi, tap Sync Now — error message appears in the Last Sync row, recovers when network returns.
  • Toggle off iCloud Sync — Refresh from iCloud section disappears.
  • Tap Refresh from iCloud → confirmation sheet → Refresh — list re-populates from CloudKit and the connection set on Mac is unchanged.
  • On Mac, edit a connection, then sync mobile — single record updates without touching the rest.
  • Background refresh while device is locked — no longer wipes connections.json (verify via Console log: "Background sync skipped: persistence load failed" instead of a successful empty merge).
  • Simulate corrupt connections.json (write garbage bytes) — load logs the error, integrity stays .loadFailed, sync is skipped, Refresh from iCloud recovers the data.

@datlechin datlechin merged commit 1848009 into main May 14, 2026
3 checks passed
@datlechin datlechin deleted the fix/ios-sync-data-loss-on-update branch May 14, 2026 00:55
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.

1 participant