The short answer
- SwiftData with CloudKit (iOS 17 and later): the least code, for new SwiftUI apps whose data fits SwiftData.
- Core Data with NSPersistentCloudKitContainer (iOS 13 and later): the mature option, for existing Core Data apps and for sharing.
- CKSyncEngine (iOS 17 and later): when you keep your own local store and want control over how and when it syncs.
And the rule that matters most: once a CloudKit schema is in production, it can only grow. You cannot delete record types or change existing fields, so the data model deserves real thought before release.
What CloudKit gives you
CloudKit stores app data in iCloud containers. Each container has a private database for each person’s own data, a public database for data every user of the app can read, and a shared database for records people share with each other. For personal data, the private database means no accounts to build and no server to run: the person’s Apple Account is the identity.
The three ways to sync
| Option | From | You write | Best for |
|---|---|---|---|
| SwiftData + CloudKit | iOS 17 | Models, almost no sync code | New SwiftUI apps with personal data |
| NSPersistentCloudKitContainer | iOS 13 | A Core Data stack | Existing Core Data apps, sharing between people |
| CKSyncEngine | iOS 17 | Your own store, plus sync handlers | Custom storage and full control |
SwiftData uses NSPersistentCloudKitContainer underneath, so the first two share the same behaviour and the same schema rules. CKSyncEngine handles scheduling, change tracking and push notifications for you, while your app keeps its data however it likes.
Setting it up
- Add the iCloud capability in Xcode, turn on CloudKit and choose or create a container. This needs an Apple Developer account with admin permissions.
- Add Background Modes with Remote notifications, so CloudKit can tell the app about changes made on other devices.
- Design a CloudKit-compatible model (below).
- Initialise the development schema, check it in the CloudKit Console, and promote it to production before release.
A CloudKit-compatible model
Apple notes that SwiftData includes a few features CloudKit does not support, such as unique constraints and non-optional relationships (Apple). In practice, that means: every property optional or with a default value, every relationship optional, and no @Attribute(.unique).
import SwiftData
@Model
final class Trip {
// CloudKit-friendly: defaults for every property, optional relationships,
// and no unique constraints.
var name: String = ""
var startDate: Date = Date.now
@Relationship(deleteRule: .cascade, inverse: \Accommodation.trip)
var accommodations: [Accommodation]? = []
init(name: String, startDate: Date) {
self.name = name
self.startDate = startDate
}
}
@Model
final class Accommodation {
var name: String = ""
var trip: Trip?
init(name: String) {
self.name = name
}
}By default SwiftData uses the first container in your entitlements. To be explicit:
let config = ModelConfiguration(cloudKitDatabase: .private("iCloud.com.example.trips"))
let container = try ModelContainer(for: Trip.self, Accommodation.self, configurations: config)The pitfalls
- The additive-only schema. After promotion, renaming or removing a field means adding a new one and migrating. Plan names and types carefully.
- No iCloud account. Some people are not signed in to iCloud, or have turned iCloud off for your app. Check the account status and keep the app working locally.
- Sync is not instant. Changes arrive in the background, sometimes seconds later. Design the interface so a late update does not surprise anyone.
- Storage belongs to the person. Private database data counts against their iCloud storage. Keep large files out, or store them as assets deliberately.
- Testing on one device. Sync bugs only show with two devices on the same account. Test that way from the first sprint.
Sharing between people
For data two or more people work on, such as a shared list or a couple’s plans, CloudKit has CKShare: one person shares records, others accept, and the records appear in their shared database. Core Data’s CloudKit integration supports sharing, and it is the usual route when collaboration is central to the app.
When CloudKit is the wrong tool
- Android or web clients need the same data. A backend of your own is usually simpler.
- The server has to act on the data, for example to send emails or run jobs.
- Data shared at large scale between many users, beyond what the public database or sharing are designed for.
CloudKit is one of the deliverables in our iOS app development work, and one of the cost drivers in what an iOS app costs.
Related: SwiftUI vs UIKit · How much an iOS app costs · iOS app development