If you want to use CoreData or SwiftData with CloudKit and build for iOS and Mac (and probably other Apple platforms) here are the things you might have to consider (i.e. things I learned the hard way):
CloudKit Environment
Xcode Debug Build & Run uses the Development environment on CloudKit. Testflight and app store releases use the Production environment. There are ways to use production environment for debug builds, but not the other way around.
Keep Environments separate
But if you both test and use your app on the same machine (hello indie Mac developer), all these use the same local ~/Library/Containers folder. This will create a big mess with your data, syncing the same local data with different CloudKit environments.
To prevent this mess I configure seperate Bundle IDs for Release and Debug builds (e.g. “ch.zoziapps.leviathan” and “ch.zoziapps.leviathan.debug”).
This creates distinct folders in ~/Libray/Containers and makes it easier to separate test and productive use of your app.
Make sure to do this for all of your apps targets.
Attention when profiling!
Environment separation is tricky as soon as you Profile your app with Instruments. The Profile target by default uses the ‘release’ build configuration because this leads to more realistic measurements. But on the CloudKit environment remains Production! This means, as soon as you profile your app, your local app data gets contaminated with development-container data, which in turn contaminates your production-data the next time you launch the executable normally.
One way to address the issue is to edit the profile scheme to use the ‘debug’ configuration.
App Extensions/Embedded Targets and App Groups
Speaking of all targets: If you have an extension that needs to access the same data, you have to place your container in an AppGroup. I would create two separate AppGroups (again, a release and a debug version) and initialize the container with the correct app group:
static func createModelContainer() -> ModelContainer {
let schema = Schema([
Item.self,
])
#if DEBUG
let groupName = "group.loremipsum.debug"
#else
let groupName = "group.loremipsum"
#endif
let modelConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
groupContainer: .identifier(groupName),
cloudKitDatabase: .automatic)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}
Both app groups need to be checked in the Capabilities section.
Configure CloudKit Framework
Also make sure that CloudKit appears under “Frameworks, Libraries and Embedded Content” (options “Always Used” and “Do not embed”) otherwise CloudKit won’t sync, at least on the Mac (I think). (Thank you JTostitos in this Reddit thread).
Deploy Schema
Don’t forget to deploy the schema (and later changes) to production in the iCloud Console.
@stefan nice article. I did not use these services so far. But is it possible to have a single app and somehow decide during runtime where you sync to? Having to separate apps is nice but it creates so much overhead. And when you want to create a service with multiple backends it would also help. Although I assume then I should not use CloudKit in the first place at all.
It is possible to enable or disable the CloudKit sync at runtime (when you create the Model). If you are using SwiftData it is as simple as passing .automatic or .none for the ModelConifguration’s cloudKitDatabase argument.