●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing●MAX — Rork Max generates native Swift for iPhone, iPad, Apple Watch, Apple TV, and Vision Pro, with 2-click App Store publishing and no Xcode required●STACK — Standard Rork builds cross-platform mobile apps with React Native (Expo); choosing between the two by use case is the key decision●FOCUS — Unlike web-first tools such as Bolt or Lovable, Rork specializes in native iOS and Android app generation●BUGS — A hands-on review reports Rork resolved about 70% of bugs without manual help, with the remaining 30% needing edits in the exported codebase●FUNDING — Rork raised $2.8M from a16z (Andreessen Horowitz)●PRICING — It is free to start, with paid plans from $25/month, so you can try before committing
Designing CloudKit Sync in a Rork Max Native App — Handling Conflicts and Deletes
You want the same data on iPhone and iPad. When you add CloudKit to a Swift app generated by Rork Max, the hard part is not saving — it is conflicts and deletes. Here are the design decisions I settled on.
You add a favorite on iPhone, but it is missing on iPad. Or something you deleted on iPad comes back to the iPhone a while later. When I added sync to a native Swift app generated by Rork Max, this kind of inconsistency was the first thing I hit.
The code to save a record to CloudKit is straightforward. The hard part is the design: conflicts when several devices write at once, and how to propagate a delete to every device. Ship with that left vague and, to the user, it looks like "an app where data spontaneously appears and disappears." From my experience as an indie developer running apps over a long stretch, what erodes trust is not crashes so much as this kind of quiet inconsistency. Here are the decisions I locked in.
First, Draw the Line Between KV Store and CloudKit
iCloud sync offers several options. For small data like settings, NSUbiquitousKeyValueStore is enough — a lightweight mechanism that syncs key-value pairs only. Its capacity is around 1 MB, and it is not suited to a collection of structured records.
For data whose count grows — items the user creates like notes, favorites, collections — put it in CloudKit with CKRecord. My line: "fixed count addressable by key, use the KV store; variable count you will want to query, use CloudKit." Make that call up front, or you will later cram records into the KV store and break.
Saving a CloudKit record looks like this:
import CloudKitstruct FavoriteRecord { let id: CKRecord.ID var title: String var updatedAt: Date}func save(_ favorite: FavoriteRecord) async throws { let db = CKContainer.default().privateCloudDatabase let record = CKRecord(recordType: "Favorite", recordID: favorite.id) record["title"] = favorite.title record["updatedAt"] = favorite.updatedAt _ = try await db.save(record)}
That part is easy. The trouble is what comes next.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Understand the boundary between NSUbiquitousKeyValueStore and CloudKit (CKRecord), and decide which to use
✦Learn the code to resolve conflicts when two devices edit the same record, using the server change token and changeTag
✦Reproduce the tombstone pattern that prevents 'zombie resurrection' when syncing deletes
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
When two devices edit the same record separately, whichever saves last silently overwrites the earlier change. The key to preventing this is the recordChangeTag each CKRecord carries. CloudKit returns a serverRecordChanged error when the server's tag and the sender's tag disagree. The point is not to swallow this error but to route it into resolution logic.
func saveWithConflictResolution(_ favorite: FavoriteRecord) async throws { let db = CKContainer.default().privateCloudDatabase do { let record = CKRecord(recordType: "Favorite", recordID: favorite.id) record["title"] = favorite.title record["updatedAt"] = favorite.updatedAt _ = try await db.save(record) } catch let error as CKError where error.code == .serverRecordChanged { // Server is newer. Compare both updatedAt values to pick a winner. guard let server = error.serverRecord, let client = error.clientRecord else { throw error } let serverDate = server["updatedAt"] as? Date ?? .distantPast let clientDate = client["updatedAt"] as? Date ?? .distantPast if clientDate > serverDate { server["title"] = client["title"] server["updatedAt"] = clientDate _ = try await db.save(server) // re-save the resolved record } // If the server is newer, do nothing (treat server as truth) }}
Choosing "last-write-wins" works here precisely because a favorite is a simple value. If you need character-level merging like a collaboratively edited document, this strategy is not enough — you need field-level diffs or version vectors. Deciding how strict to be, based on the nature of your data, is the designer's job.
Stop "Zombie Resurrection" with Tombstones
Deletion was the trickiest. You delete a record on iPad, but an iPhone that has not synced yet still holds it locally. The next time the iPhone syncs, it re-uploads its "live" local record to the server, and the item you deleted reappears on every device. That is zombie resurrection.
The fix is to treat deletion not as physical removal but as recording a delete flag (a tombstone).
// Instead of physically deleting, mark as deleted and sync itfunc softDelete(_ id: CKRecord.ID) async throws { let db = CKContainer.default().privateCloudDatabase let record = try await db.record(for: id) record["isDeleted"] = true record["updatedAt"] = Date() _ = try await db.save(record)}
On sync, each device removes records with isDeleted == true from the UI. Keep the tombstone for a set period (I chose 30 days), then physically delete it with a server-side batch. Thirty days is my estimate for every device to sync at least once and pick up the flag. Too short, and resurrection happens on a device that was offline for a long stretch.
Fetch Only the Diff with a Change Token
Fetching every record each time wastes both bandwidth and time. Save CloudKit's CKServerChangeToken and fetch only the changes since last time with CKFetchRecordZoneChangesOperation.
let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()config.previousServerChangeToken = savedToken // last tokenlet op = CKFetchRecordZoneChangesOperation( recordZoneIDs: [zoneID], configurationsByRecordZoneID: [zoneID: config])
Persisting this token locally per device is essential. Lose it and you fall back to fetching everything, which makes the first sync visibly slow for a user with thousands of records. I accidentally cleared it early on and was pulling the full set on every sync, producing a multi-second wait for data-heavy users.
What to Try Before Shipping
Once CloudKit is in, edit the same item separately on two real devices in airplane mode, then bring both back online. Whether conflict resolution and delete propagation behave as intended — that is the most effective check. Sync is finished not when it "saves," but when two devices agree without contradiction. It is unglamorous, but passing it quietly changes how much users trust the app.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.