iosswiftswiftuiswiftdata

Migrate Core Data to SwiftData in an App Group


I’m currently migrating my Core Data to SwiftData. After figuring out how to correctly apply a migration plan to evolve the data structure, I now want to move the database to App Groups. My goal is to support Widgets (and later iCloud sync).

My old apps setup placed the Core Data file into the Application Support directory and saved it in a .sqlite file.

I’ve already setup the App Group signing capability in Xcode. But, now I can’t figure out how to find the old Core Data, migrate (and evolve) it in SwiftData, and place it in the app group.

In one of Apple’s example projects they note the following, but this doesn’t seem to work for me:

This sample app uses App Groups to access shared containers and share data between the SwiftData widget extension and the Core Data host app. For an app that has the App Groups Entitlement, it persists the data store to the root directory of the app group container. For apps that evolve from a version that doesn’t have any app group container to a version that has one, SwiftData copies the existing store to the app group container.

My Code

@main
struct MyAppName: App {
    let container: ModelContainer
    let dataUrl = URL.applicationSupportDirectory.appending(path: "Something.sqlite")
    
    init() {
        let configuration = ModelConfiguration(url: dataUrl)
        
        do {
            // Create SwiftData container with migration and custom URL pointing to legacy Core Data file
            container = try ModelContainer(
                for: Foo.self, Bar.self,
                migrationPlan: MigrationPlan.self,
                configurations: configuration)
        } catch {
            fatalError("Failed to initialize model container.")
        }
    }

    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

I’ve already tried to use the ModelConfiguration with a name, but it seems to only look for a .store file and thus doesn’t copy over the Core Data contents.

let fullSchema = Schema([Foo.self, Bar.self])
        
let configuration = ModelConfiguration("Something", schema: fullSchema)

Solution

  • That line from the documentation also caught my attention, given how troublesome it can be to move the persistent store from the application support directory to an app group container directory. I created a new Xcode project using the default Core Data template to test that SwiftData automatically copies the store. Then, I added a widget extension and linked the two targets via a newly created app group. I then tried to query the data using SwiftData in the widget extension. The fetch yielded no results. I described the process in more detail on the Apple Developer Forum. I hope we can get a bit of clarity from someone from Apple there.

    So, for now, I'm migrating the traditional way. I've created a sample project for your reference here. There is also a great article about all kinds of nitty-gritty on migration: CloudKit, etc.

    init() {
        container = NSPersistentContainer(name: modelName)
        
        if FileManager.default.fileExists(atPath: originalStoreURL.path(percentEncoded: false)) {
            container.persistentStoreDescriptions.first!.url = originalStoreURL
            logger.log("Using the original store URL.")
        } else {
            // This clause covers the after-migration path and the case where the user installs a fresh copy of the app.
            container.persistentStoreDescriptions.first!.url = sharedStoreURL
            logger.log("Using the shared store URL.")
        }
        
        container.loadPersistentStores(completionHandler: handleLoadPersistentStores)
        
        /*
         In case where we want to migrate the persistent store to an app group container, we can only initiate migration after the store is loaded.
         */
        
        if container.persistentStoreCoordinator.persistentStore(for: originalStoreURL) != nil {
            try! migrateStoreToAppGroupContainer()
            
        } else {
            logger.log("Migration will not execute.")
        }
        
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
    
    
    private func migrateStoreToAppGroupContainer() throws {
        
        // Migrate
        try container.persistentStoreCoordinator.replacePersistentStore(at: sharedStoreURL, withPersistentStoreFrom: originalStoreURL, type: .sqlite)
        
        // Delete the old store
        try container.persistentStoreCoordinator.destroyPersistentStore(at: originalStoreURL, type: .sqlite)
        try FileManager.default.removeItem(at: originalStoreURL)
        
        // Load the new store
        container.persistentStoreDescriptions.first!.url = sharedStoreURL
        container.loadPersistentStores(completionHandler: handleLoadPersistentStores)
        
        logger.log("The migration was successful!")
    }
    

    Then, I thought it would be a great idea to move the main target to SwiftData. That way, the entire project will use the new framework. But I encountered this bottleneck. Check it out.