My app has been in the App Store a few months. In that time I've added a few updates to my SwiftData schema using a MigrationPlan, and things were seemingly going ok. But then I decided to add CloudKit syncing. I needed to modify my models to be compatible. So, I added another migration stage for it, changed the properties as needed (making things optional or adding default values, etc.). In my tests, everything seemed to work smoothly updating from the previous version to the new version with CloudKit. So I released it to my users. But, that's when I started to see the crashes and error reports come in. I think I've narrowed it down to when users update from older versions of the app. I was finally able to reproduce this on my end, and Core Data is throwing an error when loading the ModelContainer saying "CloudKit integration requires that all attributes be optional, or have a default value set."
Even though I did this in the latest schema. It’s like it’s trying to load CloudKit before performing the schema migration, and since it can’t, it just fails and won’t load anything. I’m kinda at a loss how to recover from this for these users other than tell them to delete their app and restart, but obviously they’ll lose their data that way. The only other idea I have is to setup some older builds on TestFlight and direct them to update to those first, then update to the newest production version and hope that solves it. Any other ideas? And what can I do to prevent this for future users who maybe reinstall the app from an older version too? There's nothing special about my code for loading the ModelContainer. Just a basic:
let container = try ModelContainer(
for: Foo.self, Bar.self,
migrationPlan: SchemaMigration.self,
configurations: ModelConfiguration(cloudKitDatabase: .automatic)
)
I figured out the solution, in case anyone else stumbles on this and is looking for an answer. It seems that the solution is to do:
do {
if let container = try? ModelContainer(
for: Foo.self, Bar.self,
migrationPlan: SchemaMigration.self,
configurations: ModelConfiguration(cloudKitDatabase: .automatic)
) {
self.container = container
} else {
self.container = try ModelContainer(
for: Foo.self, Bar.self,
migrationPlan: SchemaMigration.self,
configurations: ModelConfiguration(cloudKitDatabase: .none)
}
} catch {
// handle error
}
Essentially, if it fails the first time, disable CloudKit. This lets the SchemaMigration perform and complete and will load the container. Of course, CloudKit is disabled still, but the next time the app launches, the schema will be compatible and CloudKit will load up fine.