Lightweight migration in Swift/UI with CoreData

Hey all. I've been rewriting my Objective-C app in SwiftUI (it's going well, thanks!) and I'm hitting this issue that I thought I'd solved ages ago.

I have version 5.1 of my original ObjC app using version 11 of the Core Data model. I've created a new version 12, added a couple of new attributes, and created a mapping model from 11 to 12.

The fields I've added are booleans, and I've set them to have a default value of NO so that new entries will automatically get NO, but there are existing records that don't have that field at all, and I'd like them to also get NO (or YES, depending on something in the other fields, but I haven't figured out how to do that yet).

This is how Core Data is set up in the new Swift target:

public extension URL {
	// Returns a URL for the given app group and database pointing to the sqlite database
	static func storeURL(for appGroup: String, databaseName: String) -> URL {
		guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)?.appending(path: kCoreDataFolder) else {
			logger.error("Shared file container could not be created.")
			fatalError("CoreData: Shared file container could not be created.")
		}
		return fileContainer.appendingPathComponent("\(kCoreDataModel)")
	}
}

// MARK: - Persistence Controller
struct CoreData {
	static let shared = CoreData()
	let container: NSPersistentContainer
	init() {
		let storeURL = URL.storeURL(for: kAppGroup, databaseName: kCoreDataModel)
		let storeDescription = NSPersistentStoreDescription(url: storeURL)

		container = NSPersistentContainer(name: kCoreDataModel)
		container.persistentStoreDescriptions = [storeDescription]
		container.loadPersistentStores(completionHandler: { (storeDescription, error) in
			if let error = error as NSError? {
				logger.error("Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)")
				fatalError("CoreData: Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)")
			}
		})
		container.viewContext.automaticallyMergesChangesFromParent = true
	}

When I run the old app on the Simulator it adds some demo data (using model v11), and the app functions properly. If I then install the new version - using v12 - it fails with a Core Data error because it couldn't migrate the data. (Apologies, I cannot get the exact error at the moment as I'm part-way through redoing some other stuff and the app won't build.)

I've read somewhere that "When we use the NSPersistentContainer class to create and manage the Core Data stack, we don't need to do any additional setup work, the lightweight migration is automatically activated for us." But if that's the case, why does my app crash? I'll try and get the exact error for you...

How do I implement lightweight migration in Swift? This page suggests creating a persistent coordinator and adding a couple of options, but I can't quite figure out how to do that with the code I already have, and each time I try it seems to beat the original store so I keep going back to the above code because it works.

Accepted Reply

Right, I've figured it out (after a good night's sleep). Somehow, version 11 had one attribute as Optional, and so something in my app code had set it to nil. In the new version the fields are required so the mapping failed to map a nil into a String. The fix was to add a migration policy class, like this:

import CoreData
import Foundation

class Migrate11_12 : NSEntityMigrationPolicy
{
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		if(sInstance.entity.name == kCoreDataEvent) {
			sInstance.setValue("", forKey: kUnit)
		}
	}

Replies

Right, I've figured it out (after a good night's sleep). Somehow, version 11 had one attribute as Optional, and so something in my app code had set it to nil. In the new version the fields are required so the mapping failed to map a nil into a String. The fix was to add a migration policy class, like this:

import CoreData
import Foundation

class Migrate11_12 : NSEntityMigrationPolicy
{
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		if(sInstance.entity.name == kCoreDataEvent) {
			sInstance.setValue("", forKey: kUnit)
		}
	}