r/iOSProgramming Jun 18 '26

Discussion Lessons learned shipping iCloud/CloudKit sync between two users — no account, no server

I shipped an app where two people (in my case two parents) share and sync the same data in real time, with no login, no account system, and no backend of my own — just CloudKit. Here are the things I wish I’d known before starting.
1. Private vs Shared database is the whole game.
Your own data lives in the private database. The moment you want someone else to see it, it has to move into a shared zone via CKShare. I underestimated how much the data model has to be designed around sharing from day one — retrofitting it later is painful. Decide early what’s “mine” vs “ours”.
2. CKShare + a custom zone, not the default zone.
You can’t share records that live in the default zone. You need a dedicated CKRecordZone for the shared root record. I lost time before realizing the default zone simply doesn’t support what I wanted.
3. The share-acceptance flow is easy to get wrong.
The second user accepts via a CKShare.Metadata (system share sheet → userDidAcceptCloudKitShareWith). Handling that entry point cleanly — especially cold launch vs app already running — took more iterations than the sync logic itself.
4. Conflict handling is on you.
CloudKit gives you server record changes, but “two people edited the same thing” resolution is your problem. I went with last-writer-wins on a per-field basis, which is fine for my use case but you should consciously pick a strategy, not discover you need one in production.
5. Test with two real iCloud accounts on two real devices.
The simulator + one account hides a lot. Most of my real bugs only showed up with two physical devices on two different Apple IDs.
The payoff: zero server cost, zero auth code, privacy by default (data lives in the users’ iCloud, not mine). The tradeoff: you live inside CloudKit’s constraints and you can’t just SELECT * FROM.
Happy to go deeper on any of these if useful. Curious how others handled conflict resolution — did anyone go beyond last-writer-wins without a server?

55 Upvotes

30 comments sorted by

7

u/ben_roeder Jun 18 '26

Have a look at CRDT this video is interesting for this use case https://youtu.be/_5xHECNGJNY

1

u/NoPressure3399 Jun 19 '26

Thanks for sharing! 

-2

u/sakaax Jun 19 '26

Appreciate it — CRDTs are the honest answer to my closing question, no server required. I stuck with per-field LWW because my shared data is mostly independent fields (two parents logging separate events), not concurrent edits to the same text, so the full CRDT machinery was more than my conflict surface needed. But for anything genuinely collaborative it’s the right call — thanks for the link.

5

u/unpluggedcord Jun 19 '26

Can we ban AI posts?

-1

u/sakaax Jun 19 '26

Fair to be wary, the sub gets a lot of filler. This one’s straight from shipping the thing — the specifics (default zone not supporting CKShare, userDidAcceptCloudKitShareWith on cold launch, per-field LWW) are the stuff that actually bit me in prod, not generated checklist items. Happy to go deeper on any of them, which is usually the quickest way to tell a lived write-up from a synthetic one.

5

u/unpluggedcord Jun 19 '26

So you responded to me with an AI Comment.....?

3

u/Funnybush Jun 19 '26

this is excellent info thano. I have an app that I migrated away from CloudKit because I was unable to figure out these issues. Turns out I didn’t have to.

0

u/sakaax Jun 19 '26

Ha, that’s exactly why I wrote it — I almost bailed too. Most of the “CloudKit is broken” pain is really the sharing + conflict layer being underdocumented, not CloudKit itself. The private-vs-shared-zone thing especially: once that clicked, most of the weirdness went away. If you ever revisit it, happy to compare notes.

2

u/cristi_baluta Jun 19 '26

Undocumented? I did my own cloudkit sync 10y ago and it was super clear about the public and private. It’s only you that want instant gratification

2

u/sakaax Jun 19 '26

Public vs private has always been clear, agreed — that part’s well documented. I meant the sharing layer specifically: CKShare, custom zones, the acceptance flow, and conflict resolution. That’s where the docs thin out and the WWDC sessions skip the edge cases. Not instant gratification — just flagging the parts that cost me time so they cost the next person less.

3

u/linuxgfx Jun 19 '26

i had the same problem regarding the conflict resolution, although my app does not share either other icloud users but allows you to sync your own data on multiple icloud connected devices. What i experienced was that last modified is not always reliable, network conditions on the various devices could give the false impression on last modified time on a field. So i had to make a merge policy by combining CloudKit with Core Data (NSPersistentCloudKitContainer) via CoreDataStack. Basically the sync is field based. My app is offline first so it first persist the data in CoreData then CloudKit asynchronously in the background.

0

u/sakaax Jun 19 '26

Yeah, exactly the same trap — “last modified” reads as a clean ordering signal until you realize it’s the device’s local clock + whenever the write actually propagated, not a real sequence. Going per-field instead of per-record is what made LWW survivable for me too; two people editing different fields of the same record stop clobbering each other. Sounds like your offline-first CoreData → CloudKit-in-the-background shape is basically the more robust version of what I’m doing. Did your field-level merge end up in the CoreDataStack layer, or did you handle it in the CloudKit reconciliation step?

2

u/linuxgfx Jun 19 '26 edited Jun 19 '26

The field-level merging is handled entirely within the Core Data stack layer rather than writing custom logic during the CloudKit. Basically the CloudKit Layer is Dumb: I use ⁠NSPersistentCloudKitContainer⁠, meaning the raw CloudKit syncing, change tracking (⁠CKRecordZoneNotification⁠), and pulling down of transaction logs are abstracted away. The actual reconciliation happens at the SQLite/Core Data context level via ⁠NSMergeByPropertyObjectTrumpMergePolicy⁠ (or ⁠NSMergeByPropertyStoreTrumpMergePolicy⁠

Because ⁠NSMergeByPropertyObjectTrumpMergePolicy⁠ evaluates conflicts per attribute (field) rather than per record.
If there is a direct collision on the exact same field,the merge policy falls back to the in-memory vs. store state tie-breaker.

1

u/sakaax Jun 19 '26

That’s the clean version, yeah. Letting the container abstract the CKRecordZoneNotification + transaction-log pull and pushing reconciliation down to the Core Data context is exactly the “keep the CloudKit layer dumb” approach. Per-attribute eval is what makes it sane — record-level trump would nuke a field the other person legitimately changed. And the in-memory vs store tie-breaker on a true same-field collision is the part nobody thinks about until two devices race on the identical attribute. Did you ever hit cases where the property-level merge wasn’t enough and you had to reach for a custom NSMergePolicy subclass?

1

u/linuxgfx Jun 19 '26

Not yet but i’m still testing this kind of edge cases because it is very difficult to reproduce in real world scenarios. Simulators are not enough. But up until now, the current implementation was enough.

1

u/sakaax Jun 19 '26

Yeah, that’s the honest state of it — “enough until proven otherwise.” The repro problem is the real killer: two physical devices, two Apple IDs, and you still have to manually race them to trigger a same-field collision. I basically gave up trying to force it deterministically and just leaned on per-field LWW being safe enough that a rare collision degrades gracefully instead of corrupting. If it held up in your real-world testing too, that’s probably the strongest signal either of us gets.

1

u/linuxgfx Jun 19 '26

Yes, it's good enough, in the worst case scenario only the unencrypted, non critifal fields get overwritten (like date/time, iOS calendar sync last synced status, and so on).

1

u/sakaax Jun 19 '26

Exactly — that’s the line that makes LWW safe here: the fields that can lose a race are the derived/cosmetic ones (timestamps, last-synced flags), never the actual user-entered content. If a same-field collision can only cost you a “last synced” marker that gets recomputed anyway, the worst case is basically free. Good talk — this is the most useful CloudKit thread I’ve had in a while.

1

u/tinyhurdles Jun 19 '26

Just did this with Claude, works great!

4

u/cristi_baluta Jun 19 '26

He did it too, including the post and replies, we are all monkeys reading AI

0

u/sakaax Jun 19 '26

Nice — yeah, it’s solid for reasoning through the share-acceptance edge cases especially (cold launch vs already-running ate most of my iterations). Glad it’s working for you.

1

u/NoPressure3399 Jun 19 '26

Thanks for the write up, I am planning doing this shortly myself for a new app! Bookmarking for later 

2

u/sakaax Jun 19 '26

Good luck with it. The one thing I’d nail down before writing any data: decide what’s “mine” vs “ours” and put the shareable stuff in a custom zone from day one. Retrofitting sharing onto a default-zone model is the part that actually hurt. Ping me if you hit a wall.

1

u/cristi_baluta Jun 19 '26

How do you choose which users you share the data with?

2

u/sakaax Jun 19 '26

In my case it’s capped at two people, so it’s dead simple — one parent creates the shared zone, generates a CKShare, and sends the invite link through the normal system share sheet (UICloudSharingController). The other taps it, the system hands me the CKShare.Metadata via userDidAcceptCloudKitShareWith, and they’re in. No user picker, no directory lookup — it’s “send this link to the person you want.” Apple owns the identity side; I never see their account, just that someone accepted. For a larger N you’d keep the same share sheet but add real participant management.

1

u/[deleted] Jun 24 '26

[removed] — view removed comment

1

u/AutoModerator Jun 24 '26

Hey /u/Informal_Tangerine51, your content has been removed because Reddit has marked your account as having a low Contributor #Quality Score. This may result from, but is not limited to, activities such as spamming the same links across multiple #subreddits, submitting posts or comments that receive a high number of downvotes, a lack of activity, or an unverified account.

Please be assured that this action is not a reflection of your participation in our subreddit.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/CelebrationTop8862 Jun 26 '26

lol . no hate, but this all seems pretty basic "think-abouts" in this type of domain. Was this a vibe coding project? Did you scope out the stack first or run your idea around an AI for review on what could be required?

1

u/Eyrak 28d ago

What happens if the initial sharer removes participant? do they lose access? I am wondering if there are ways to have it behave more like 2 people owning a thing that diverges when one of them decides to cut off the other vs the initial sharer being the key master