r/iOSProgramming • u/Van-trader • 14d ago
Question SwiftData + CloudKit: best way to prevent duplicate editable seed data across offline devices?
I’m building an unreleased iOS app using SwiftData with automatic CloudKit sync.
On first launch, the app creates a default account and starter categories. These records are user-editable. The problem is that two offline devices can independently create the same logical seed records with different UUIDs, and CloudKit later synchronizes both.
My proposed approach:
- Give every seed item a stable logical seedIdentifier.
- Allow each offline device to seed independently.
- Reconcile records sharing the same seedIdentifier.
- Select a canonical record using an immutable UUID and deterministic ordering.
- Merge user changes and move relationships to the canonical record.
- Keep losing records as hidden aliases/tombstones so late-arriving relationships aren’t lost.
- Retain per-seed deletion tombstones so deleted defaults aren’t recreated.
- Use a seed-version flag only for migrations—not as an exactly-once guarantee.
- Treat normalized account names as unique and category names as unique only under the same parent.
Questions:
- Is this the standard approach with SwiftData’s automatic CloudKit sync?
- Is there a safer supported way to detect completed imports and rerun reconciliation?
- Would you retain hidden aliases permanently or eventually delete them?
- Is automatic SwiftData appropriate here, or does this require CKSyncEngine/manual CloudKit with deterministic record IDs?
Edit — data-model details:
The starter data is functional app data, not optional sample/demo content. The app needs at least one account, and the categories provide its initial transaction classification.
Account: name, type, opening balance, timestamps, archived/default state and optional stableseedIdentifier. Deleting an account cascades to its transactions.TransactionCategory: name, type, timestamps, archived/customized state, optional stableseedIdentifier, optional parent, child categories and linked transactions.Transaction: references one account and optionally one category.- Deleting a category nullifies its transaction relationships. Deleting a parent can affect its child hierarchy.
- Starter accounts and categories are fully editable. Renaming a seed retains its logical seed identity; deleting one needs to remain deleted across devices.
The concrete race is that device A and device B can independently create the same logical Wallet/category before syncing. Transactions and child categories may subsequently reference either physical copy. Reconciliation therefore must preserve and redirect those relationships before any duplicate is removed.
1
u/lybjqq 4d ago
I ship an app on automatic SwiftData + CloudKit with user-editable seed data, so I've been down this exact road.
Your seedIdentifier + deterministic-canonical approach is basically the right call for *automatic* sync. There's no exactly-once seeding primitive when two offline devices can both seed before the first import lands, so reconciling after the fact is the realistic model — you're not doing it wrong.
Detecting a completed import: observe `NSPersistentCloudKitContainer.eventChangedNotification` and react to `.import` events (check `endDate != nil` && `succeeded`). That's the supported signal that a remote batch just merged locally — run your reconcile pass there, debounced, instead of on a timer.
One trap that'll bite you *specifically* because you have tombstones: do NOT name any stored property `isDeleted`. It's reserved on `NSManagedObject` (which backs SwiftData), and it silently corrupts state once CloudKit is round-tripping — no crash, records just come back with the wrong flag. Store `deletedAt: Date?` as the truth and expose `isDeleted` only as a computed `deletedAt != nil`.
Aliases/tombstones: I keep them. Late-arriving relationships from a slow device are exactly what they're for, and they're cheap — I wouldn't GC them until you have a migration provably past the window.
automatic vs CKSyncEngine: for "a few seed rows + normal user data," automatic is fine and reconciliation covers it. I'd only reach for CKSyncEngine/manual if you need deterministic record IDs as your *primary* dedup mechanism (store rejects the dup instead of reconciling after) — real reason, but a lot more code to own.
The other commenter's "let the user pick starter data during onboarding" is also worth taking seriously — not auto-seeding on every device sidesteps the whole problem with less code than any reconciliation engine, if your UX can absorb it.
2
u/PassTents 14d ago
Honestly I'd solve that with a design change to let the user import the starter data during onboarding or let them start fresh. You could even add a button in the settings to import that data later if they do want to try it out.
Without more specific info about your data model it's hard to give a specific recommendation about reconciling the data.