I’ve finally managed to get the migration working. What I did was basically:
Using FileManager to check whether the .sqlite file exists at the old application directory.
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.
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.
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!