Search code examples
swiftcore-datacore-data-migration

Populate new UUID attribute during Core Data migration


I’ve realized I need id’s for all of my Core Data entities to fetch by those id’s. As the NSManagedObjectID can change, I want to add a custom id attribute of the type UUID.

Optional vs. Non-Optional UUID

First I planned to add the id as a non-optional UUID. Though, as CloudKit seems to require that all attributes are optional anyway, it seems like a good idea to mark this new UUID as optional (should I add iCloud sync later). This makes it also eligible for a lightweight Core Data migration.

How To Populate Attribute?

Even though, the id is marked optional in the Core Data Model, I want to ensure all of the entities (both existing and new ones) actually have an id/UUID saved!

How do I populate the new UUID attribute during the Core Data migration? Do I have to use a manual migration for this?


Solution

  • After testing the approach by Tom Harrington, I couldn’t find out why my mapping model isn’t working for some unknown reason. I’ve redone it so many times and checked every possible part.

    Nevertheless, I’ve decided to go for a lightweight migration now. It’s recommended by Apple anyway and this is future proof for adding CloudKit support later on. As CloudKit requires attributes to be optional or have a default value set, if non-optional. As both options don’t really make sense for an ID, I have to handle this myself to ensure the id’s are unique anyway.

    My Solution

    I’m basically following Michael Tsai’s suggestion now, but wanted to elaborate a little further.

    First I’ve created a new version of my Core Data Stack via Editor > Add Model Version… and edited the new version.

    Next I’ve set the active Core Data Stack to the new version via the editor sidebar. Besides that I’ve also set a model version identifier in in the sidebar:

    To catch the database update and perform the actual migration I’ve added a custom implementation when creating my Core Data Controller.

    container = NSPersistentContainer(name: "Migration_Test")
    
    
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Core Data Loading Error: \(error), \(error.userInfo)")
        }
    })
    container.viewContext.automaticallyMergesChangesFromParent = true
    
    
    // MANUAL MIGRATION (if needed)
    // Get User Default for Last Migration Version (0 if not set)
    let lastMigration = UserDefaults.standard.integer(forKey: "lastMigrationVersion")
    // Get Core Data Model Version Identifier (Set in Sidebar)
    let modelIdentifier = container.viewContext.persistentStoreCoordinator?.managedObjectModel.versionIdentifiers.first as? String
    let currentModelVersion = Int(modelIdentifier ?? "") ?? 0
    
    // Check if migration is needed
    if lastMigration < currentModelVersion {
        do {
            // Perform Actual Migration
            try ManualMigration().migrate(from: lastMigration, to: currentModelVersion, in: container.viewContext)
            
            // Save Latest Migration Version to User Defaults
            UserDefaults.standard.set(currentModelVersion, forKey: "lastMigrationVersion")
        } catch {
            print("Failed Migration to Version \(currentModelVersion)")
        }
    }
    

    Inside my ManualMigration().migrate() class/function I simply check which version I want to migrate to. In my case I’ve then iterated trough all my entities that changed via a fetch request that fetches all items of each give entity and saves a new UUID.