Exploring CloudKit and CKSyncEngine for my SwiftUI App

Joseph Humfrey

Joseph Humfrey

16th November 2024

If you're building a SwiftUI app in 2024, you'll face a key decision: how to handle data persistence and sharing. Modern apps need collaboration—users expect to share and sync their content seamlessly across devices and with others. But implementing robust sync in iOS isn't straightforward. Let me walk you through my journey with CloudKit, and why I landed on a surprising solution.

I’m working on a time planning app in SwiftUI with a simple hierarchical structure: a Schedule (top-level "document") contains multiple Days, and each Day has multiple Events. A key feature is iCloud syncing and sharing—users should be able to collaborate on Schedules seamlessly.

Since my goal is to stick to Apple's ecosystem, I turned to CloudKit. A major advantage is that users don't need a separate login - they're already signed into iCloud on their devices, making it feel like a seamless, native Apple experience.

The CloudKit/Sharing Dilemma

Option 1: SwiftData

For my brand new app, I'm trying to use the latest Apple recommended tech. It seeeems promising but doesn’t yet support sharing, which I think it a critical feature for my app. Plus, I’ve heard it still has growing pains, so I'd be concerned about using it in production at this stage.

Option 2: Core Data + CloudKit

This is where I initially made the most progress, since it seemed like the most mature option. It's also the most popular one that I see mentioned online: NSPersistentCloudKitContainer supports syncing and recently added sharing.

As a newcomer to the Apple backend ecosystem, I think I assumed that CloudKit directly synced as Core Data behind the scenes. Unfortunately they're two separate systems, and although Apple handles most of the translation for you, there are still a bunch of intricacies that can trip you up. So, you still need to understand both systems to use them effectively.

You need to set up Core Data model files or manually create NSManagedObject subclasses, then handle synchronization between your app's models and Core Data entities. This creates a dilemma - either use pure NSManagedObject subclasses and lose access to SwiftUI's modern @Observable macro, or maintain separate model layers.

So, when you create a Schedule in my app, it flows through multiple layers: Schedule (your @Observable app model) gets mapped to ScheduleEntity (Core Data), which then gets translated to a CKRecord (CloudKit) behind the scenes.

I did get synchronisation working, but sharing via iCloud is when the frustrations began. I thought it would be relatively straightforward since I already had all the data in iCloud, and that it would be a matter of telling it to share a specific root object in a hierarchy or a set of objects.

Although it seems to be possible to share Core Data via CloudKit as explained by this Apple documentation, it seems like yet another layer of complexity.

Option 3: CloudKit only?

When I realised that CloudKit and Core Data were two completely different technologies that Apple has bridged, it made me start to wonder why I was bothering with Core Data at all. Local persistence in my app is pretty trivial: I don't expect users to accumulate a vast quantity of data. I can store each Schedule to be a small JSON file.

However, the CloudKit API is very complex - NSPersistentCloudKitContainer is doing a lot of heavy lifting for you.

Enter CKSyncEngine

Diagram of Core Data + CloudKit vs CloudKit-only tech stacks

After more digging, I discovered CKSyncEngine, a newer CloudKit framework from Apple, introduced in WWDC 2023. CKSyncEngine simplifies syncing by removing Core Data entirely—no translating NSManagedObject into CloudKit records. That’s one less complex layer in the stack, which I love. Syncing is inherently tricky, so cutting out Core Data removes one huge source of potential bugs and confusion.

The setup isn’t trivial, but the documentation feels less daunting. Apple’s WWDC talk and the sample project are decent starting points. Jordan Morgan from Superwall also published this helpful guide.

Syncing is complex, but CKSyncEngine’s direct CloudKit integration makes it more manageable. No dual-layered technologies fighting for control. I hated having to learn Core Data and CloudKit when all I wanted was iCloud sharing to "just work." CKSyncEngine strips this down to one API. It's still not as simple as the idealised SwiftData approach, but it's the best option I've found so far. There’s surprisingly little written about this approach. Hopefully, CKSyncEngine gains traction because it feels like a more modern and efficient way to handle iCloud syncing.

The Future

I can’t help but feel a little outraged that Apple hasn’t made iCloud sharing easier. The “ideal” Apple app, with CloudKit and collaboration, should be straightforward to build—yet it's not. Maybe SwiftData will eventually support sharing and mature into the obvious choice for new apps. Until then, CKSyncEngine seems like the cleanest path forward.