SwiftData with CloudKit failing to migrate schema

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)
)

Accepted Reply

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.

Replies

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.

If your migration plan includes a custom migration, this solution no longer works in iOS 17.4 and will crash (not in the catch, but in the ModelContainer init). I submitted feedback on this issue: FB13694972. It's easily reproducible. The first attempt to initialize the ModelContainer will fail normally and throw an error, as expected. The second one just crashes and complains about the model not being compatible with CloudKit, even though it's set to .none. Before iOS 17.4 works fine.

Thanks for making this post. I've been trying to get SwiftData custom migration work with CloudKit for more than a week now and it seems to me it's not compatible with CloudKit (works fine with just SwiftData, but with CloudKit the willMigrate and didMigrate are never called). Better to use custom code I guess to change user data.

  • Actually seems that the above described solution works with the latest iOS and macOS, so should take my words back :)

Add a Comment

I'm experiencing what @mrtnmgi describes too—willMigrate and didMigrate are never called. I have it reproducing in a repeatable test which I've attached to FB13711459.