Migrate Core Data to SwiftData in an App Group (& CloudKit)

Hello, I’m upgrading my app from Core Data to SwiftData. Due to my old setup the Core Data store has an explicitly name like „Something.sqlite“, because it was defined via NSPersistentContainer(name: "Something") before switching to SwiftData.

Now my goal is to migrate the Core Data stack to SwiftData, while moving it to an App Group (for Widget support) as well as enable iCloud sync via CloudKit.

Working Migration without App Group & CloudKit

I’ve managed to get my migration running without migrating it to an App Group and CloudKit support like so:

@main
struct MyAppName: App {
    let container: ModelContainer
    
    init() {
        // Legacy placement of the Core Data file.
        let dataUrl = URL.applicationSupportDirectory.appending(path: "Something.sqlite")
        
        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: ModelConfiguration(url: dataUrl))
        } catch {
            fatalError("Failed to initialize model container.")
        }
    }

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

How To Migrate to App Group & CloudKit?

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)

Can someone help me how to do this migration or point me into the right direction? I can’t find anything relating this kind of migration …

Answered by DTS Engineer in 790031022

It sounds like you've successfully migrated your data from CoreData to SwiftData. To summarize, you successfully migrated from CoreData to SwiftData, but you're having trouble syncing the new SwiftData container due to the configuration and the default .store name created by SwiftData after migrating from a .sqlite file. However, you're encountering issues syncing the new SwiftData container due to the configuration changes and the default .store name created by SwiftData during the migration.

Specifically, SwiftData automatically generates a .store file after migration from a .sqlite file. But in your case, you want to keep the existing .sqlite file name. Here are the steps you can follow:

  1. Update your SwiftData configuration: After completing the migration, you need to specify the file name extension to SwiftData. To do this, change the name property in the ModelConfiguration to include the .sqlite extension, after you rename it:
let configuration = ModelConfiguration("Something.sqlite", schema: fullSchema)
  • This step is crucial if you want to keep the existing .sqlite file name if that’s what you want to do.
  1. Move and open the new database: After adjusting the configuration, you'll need to move the new .store database file (which was created during migration) and rename it to ".sqlite". Then, open it using the updated configuration.

Following these two steps should get you back on track.

Now, to add iCloud support to the existing SwiftData container, follow this guide: https://developer.apple.com/documentation/swiftdata/syncing-model-data-across-a-persons-devices.

I believe you are almost there. Your modifications to the model were correct. If you could post your model here for review, it would be helpful. Additionally, ensuring that you have correctly set up iCloud support in your Xcode project is essential.

Specifically, in your Xcode project, you need to:

  • Enable iCloud CloudKit services
  • Add the SwiftData container to the iCloud container list

Let me know if you have any further questions or if there are any errors in your model configuration.

It sounds like you've successfully migrated your data from CoreData to SwiftData. To summarize, you successfully migrated from CoreData to SwiftData, but you're having trouble syncing the new SwiftData container due to the configuration and the default .store name created by SwiftData after migrating from a .sqlite file. However, you're encountering issues syncing the new SwiftData container due to the configuration changes and the default .store name created by SwiftData during the migration.

Specifically, SwiftData automatically generates a .store file after migration from a .sqlite file. But in your case, you want to keep the existing .sqlite file name. Here are the steps you can follow:

  1. Update your SwiftData configuration: After completing the migration, you need to specify the file name extension to SwiftData. To do this, change the name property in the ModelConfiguration to include the .sqlite extension, after you rename it:
let configuration = ModelConfiguration("Something.sqlite", schema: fullSchema)
  • This step is crucial if you want to keep the existing .sqlite file name if that’s what you want to do.
  1. Move and open the new database: After adjusting the configuration, you'll need to move the new .store database file (which was created during migration) and rename it to ".sqlite". Then, open it using the updated configuration.

Following these two steps should get you back on track.

Now, to add iCloud support to the existing SwiftData container, follow this guide: https://developer.apple.com/documentation/swiftdata/syncing-model-data-across-a-persons-devices.

I believe you are almost there. Your modifications to the model were correct. If you could post your model here for review, it would be helpful. Additionally, ensuring that you have correctly set up iCloud support in your Xcode project is essential.

Specifically, in your Xcode project, you need to:

  • Enable iCloud CloudKit services
  • Add the SwiftData container to the iCloud container list

Let me know if you have any further questions or if there are any errors in your model configuration.

Accepted Answer

I’ve finally managed to get the migration working. What I did was basically:

  1. Using FileManager to check whether the .sqlite file exists at the old application directory.
  2. If that’s the case I can assume in my case that the migration wasn’t done, because after the migration this file will be deleted.
  3. The migration itself loads the old NSPersistentContainer and migrates it to the app group with the coordinators replacePersistentStore(at: appGroupURL, withPersistentStoreFrom: legacyDataURL, type: .sqlite) function.
  4. Remove all the old .sqlite files with the FileManagers .removeItem(at: legacyDataURL) function.

That whole migration check and actual location-migration is run before initializing the SwiftData ModelContainer, which then points to the app group url via ModelConfiguration(url: appGroupURL).

SwiftData will then automatically perform the SchemaMigrationPlan.

IMPORTANT: Please note that you need to keep the old .xcdatamodel file in your project, otherwise it will fail to create the Core Data container!

My Code

Following the Adopting SwiftData for a Core Data app example code from Apple, I’ve now moved my ModelContainer to an actor that shares the container via a singleton with the app and widget.

@main
struct MyAppName: App {
    // App SwiftData Model Container
    let sharedModelContainer = DataModel.shared.modelContainer

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}
actor DataModel {
    /// Singleton for entire app to use.
    static let shared = DataModel()
    
    /// Legacy location of the Core Data file.
    private let legacyDataURL = URL.applicationSupportDirectory.appending(path: "MyApp.sqlite")
    /// Location of the SwiftData file, saved in SQLite format.
    private let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.domain.myapp")!.appendingPathComponent("MyApp.sqlite")
    
    
    private init() {
        // Checks for old Core Data migration before loading SwiftData.
        checkCoreDataMigration()
    }
    
    nonisolated lazy var modelContainer: ModelContainer = {        
        let configuration = ModelConfiguration(url: appGroupURL)
        
        do {
            return try ModelContainer(
                for: Foo.self, Bar.self,
                migrationPlan: MigrationPlan.self,
                configurations: configuration)
        } catch {
            fatalError("Could not create SwiftData ModelContainer: \(error)")
        }
    }()
    

    nonisolated private func checkCoreDataMigration() {
        // If old store does exist perform the migration to App Group!
        // We can expect that the old store only exists if not yet migrated, because the migration deletes all old files.
        if FileManager.default.fileExists(atPath: legacyDataURL.path(percentEncoded: false)) {
            migrateCoreDataToAppGroup()
        }
    }
    
    nonisolated private func migrateCoreDataToAppGroup() {
        let container = NSPersistentContainer(name: "MyApp")
        let coordinator = container.persistentStoreCoordinator
        
        // 1. Migrate old Store
        do {
            // Replaces Application Support store with the one in the App Group.
            try coordinator.replacePersistentStore(at: appGroupURL, withPersistentStoreFrom: legacyDataURL, type: .sqlite)
        } catch {
           print("Error replacing persistent store in App Group: \(error)")
        }
        
        // 2. Delete old Store files
        NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: legacyDataURL, options: .forDeleting, error: nil) { url in
            do {
                try FileManager.default.removeItem(at: legacyDataURL)
                try FileManager.default.removeItem(at: legacyDataURL.deletingLastPathComponent().appendingPathComponent("MyApp.sqlite-shm"))
                try FileManager.default.removeItem(at: legacyDataURL.deletingLastPathComponent().appendingPathComponent("MyApp.sqlite-wal"))
            } catch {
                print("Error deleting persistent store at Application Support directory: \(error)")
            }
        }
    }
}

NOTE: Widget extensions may create an App Group store before the migration could happen by opening the app. Therefore I'm replacing the persistent store at the App Group location.

Please note that this code doesn’t migrate to CloudKit yet!

Migrate Core Data to SwiftData in an App Group (& CloudKit)
 
 
Q