Local SwiftData to CloudKit migration

Hi, I've been working on an app that - so far - has only had to deal with offline data store.

The usage is fairly simple: the user picks their favourite tv shows and networks, and they get persisted with SwiftData with all the correct relationships.

Now, I would like to migrate the local storage of the users to a private CloudKit db, but every time I upgrade the app, the data seems to disappear completely.

This is the snippet evolved through the attempt of migrating the data:

Current Production code

public static func makeModelContainer() -> ModelContainer {
        do {
            let processRequiresInMemory = ProcessInfo.processInfo.arguments.contains("inMemoryDatabasePreferred")

            let modelConfiguration = ModelConfiguration(
                isStoredInMemoryOnly: processRequiresInMemory,
                groupContainer: .automatic,
                cloudKitDatabase: .none
            )

            let modelContainer = try ModelContainer(
                for: Country.self,
                Episode.self,
                Movie.self,
                Season.self,
                Show.self,
                Network.self,
                NetworkSubscription.self,
                migrationPlan: AppModelMigrationPlan.self,
                configurations: modelConfiguration
            )

            return modelContainer
        } catch {
            fatalError("Could not initialize model container")
        }
    }

Testing CloudKit enabled

public static func makeModelContainer() -> ModelContainer {
        do {
            let processRequiresInMemory = ProcessInfo.processInfo.arguments.contains("inMemoryDatabasePreferred")

            let modelConfiguration = ModelConfiguration(
                "synced",
                isStoredInMemoryOnly: processRequiresInMemory,
                groupContainer: .automatic,
                cloudKitDatabase: .automatic
            )

            let modelContainer = try ModelContainer(
                for: Country.self,
                Episode.self,
                Movie.self,
                Season.self,
                Show.self,
                Network.self,
                NetworkSubscription.self,
                migrationPlan: AppModelMigrationPlan.self,
                configurations: modelConfiguration
            )

            return modelContainer
        } catch {

            fatalError("Could not initialize model container")
        }
    }
}

The differences, which I don't understand fully because I could not find documentation, are:

  • ModelContainer(...) -> ModelContainer("synced", ...)
  • cloudKitDatabase, from none to automatic.

I have the feeling that changing the name of the configuration also changes some reference to the db itself, but if I were not to change the name, the app would have crashed because unable to migrate.

What's the best approach to take here?

Answered by DTS Engineer in 789987022

Unresolved error loading container Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Unable to find a configuration named 'default' in the specified managed object model.}

It seems that you are hitting a SwiftData bug – When CloudKit integration is enabled, creating a ModelContainer with a migration plan triggers an error.

Since WWDC is there, I'd suggest that you try with the latest beta. If the issue is still there, please file a feedback report for the SwiftData team to investigate.

For an immediate workaround, you might consider the following flow:

  1. Create two model containers, container1 for the local store and container2 for the CloudKit store. For container2, use a configuration name ("synced" in your case) to differentiate the CloudKit store from the local one, and don't use migration plan, which avoids the error.

  2. Migrate the data with your own code, which includes fetching data from container1, transforming it if necessary, and saving the transformed data to container2. The data should then be synchronized to CloudKit.

  3. After the data is all added to container2, you can release container1, and remove the local store.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I've got some news, which are discomforting: I can drive from local to cloudkit if I relaunch the app with changes, simulating multiple updates.

In short, if I follow these passages, everything seems to work:

  • Remove any cloudkit entitlement
  • Migrate the local swiftData to a CloudKit-compatible schema
  • Re-add the CloudKit entitlement
  • Migrate the ModelConfiguration to use the preferred cloudKitDatabase

This is definitely not something I can do, as I there must be a way to execute all these steps in one go?

It also seems that a cloudKitDatabase: .none is not respected.

let fromSchema = Schema(versionedSchema: ModelSchema_050624.self)
                let starterConfiguration = ModelConfiguration(
                    nil,
                    schema: fromSchema,
                    isStoredInMemoryOnly: false,
                    allowsSave: true,
                    groupContainer: groupContainer,
                    cloudKitDatabase: .none
                )

                let modelContainer = try ModelContainer(
                    for: Country.self,
                    Episode.self,
                    Movie.self,
                    Season.self,
                    Show.self,
                    Network.self,
                    NetworkSubscription.self,
                    migrationPlan: AppModelMigrationPlan.self,
                    configurations: starterConfiguration
                )
                return modelContainer

If the CloudKit entitlement is active (checked), this code throws:

Thread 1: Fatal error: Could not initialize model container SwiftDataError(_error: SwiftData.SwiftDataError._Error.configurationSchemaNotFoundInContainerSchema)

If the CloudKit entitlement is not active, this code migrates successfully.

I feel like you should just step back a step, and go back to the where you just need to change .none to .automatic, don't add a name "synced" because that will change the name/path to the local file, that is why your data is getting lost.

When you do that, if you see that app is crashing, look at the logs, and why it is crashing. CloudKit does have some requirements on the data, like most of the fields should be nilable or have default values, references between models should be defined both ways, and there could not any unique constraints. I am sure, if you see a crash, it is because one of those conditions are not met. Which will be straightforward to fix by changing a model a little bit.

Accepted Answer

Unresolved error loading container Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Unable to find a configuration named 'default' in the specified managed object model.}

It seems that you are hitting a SwiftData bug – When CloudKit integration is enabled, creating a ModelContainer with a migration plan triggers an error.

Since WWDC is there, I'd suggest that you try with the latest beta. If the issue is still there, please file a feedback report for the SwiftData team to investigate.

For an immediate workaround, you might consider the following flow:

  1. Create two model containers, container1 for the local store and container2 for the CloudKit store. For container2, use a configuration name ("synced" in your case) to differentiate the CloudKit store from the local one, and don't use migration plan, which avoids the error.

  2. Migrate the data with your own code, which includes fetching data from container1, transforming it if necessary, and saving the transformed data to container2. The data should then be synchronized to CloudKit.

  3. After the data is all added to container2, you can release container1, and remove the local store.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Local SwiftData to CloudKit migration
 
 
Q