iosswiftcore-datamigrationcloudkit

NSPersistentCloudKitContainer migration with custom NSEntityMigrationPolicy


Recently, I have made quite extensive changes to my schema and need to migrate my CoreData + CloudKit model to a new version. The changes require me to use a custom NSEntityMigrationPolicy because I have a rather complex mapping between some old entities and new entities.

  1. I have added a new model version. Deleted some entities, and added some entities.
  2. I have defined the NSEntityMigrationPolicy with createDestinationInstances(forSource:in:manager:) and createRelationships(forDestination:in:manager:).
  3. I have created a new Core Data Mapping Model. (This might be unnecessary. If so, how to associate my entities with migration policy programmatically?)
  4. Within the Core Data Mapping Model, I have specified Custom Policy for all entities.
  5. In my container setup, I added two options, as below:
storeDescription.setOption(false as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)

storeDescription.setOption(false as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)

I mostly followed a Core Data Heavyweight Migration guide. (It's not for CloudKit specifically.)

When I run my app, I am getting the most basic of errors:

The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store.

So, I guess that migration wasn't even attempted, which makes sense because I set NSMigratePersistentStoresAutomaticallyOption to false.

I tried to go beyond what I could find on the web and attempted to manually initialize migration:

let sourceModel = container.persistentStoreCoordinator.managedObjectModel

guard let modelURL = Bundle.main.url(forResource: "MyModelName", withExtension: "xcdatamodeld") else {
    fatalError("Unable to locate model file.")
}

guard let destinationModel = NSManagedObjectModel(contentsOf: modelURL) else {
    fatalError("Unable to load destination model.")
}
        
let migrationManager = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel)

let mappingModel = NSMappingModel(from: [Bundle.main], forSourceModel: sourceModel, destinationModel: destinationModel)!

migrationManager.currentEntityMapping.entityMigrationPolicyClassName = "MyMigrationPolicyClassName"

I am then stuck at the migrateStore(from:type:mapping:to:type:) method. It seems to target only local storage, not CloudKit. Otherwise, what do I provide for URLs and types?

My question is, how do I implement custom logic for CoreData with CloudKit migration?


Solution

  • I managed to solve the issue by adapting progressive Core Data migration model as explained by William Boles.

    This implementation works with NSPersistentCloudKitContainer. As far as I know, you do not need to set storeDescription.type for the container because NSPersistentCloudKitContainer manages the stores under the hood.

    After loading the stores, you will need to initialize the schema, like so:

    do {
        try self.container.initializeCloudKitSchema(options: [])
    } catch let error {
        fatalError("###\(#function) failed to initialize CloudKit schema due to error: \(error.localizedDescription)")
    }
    

    This code will not work if your development device/simulator is not logged into iCloud.

    Then, you will also need to open the CloudKit console and deploy your schema to production.