Starting point
I have an app that is in production that has a single entity called CDShift
. This is the class:
@Model
final class CDShift {
var identifier: UUID = UUID()
var date: Date = Date()
...
}
This is how this model is written in the current version.
Where I need to go
Now, I'm updating the app and I have to do some modifications, that are:
- add a new entity, called
DayPlan
- add the relationship between
DayPlan
andCDShift
What I did is this:
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[CDShift.self]
}
@Model
final class CDShift {
var identifier: UUID = UUID()
var date: Date = Date()
}
}
To encapsulate the current CDShift
in a version 1 of the schema. Then I created the version 2:
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[CDShift.self, DayPlan.self]
}
@Model
final class DayPlan {
var identifier: UUID = UUID()
var date: Date = Date()
@Relationship(inverse: \CDShift.dayPlan) var shifts: [CDShift]? = []
}
@Model
final class CDShift {
var identifier: UUID = UUID()
var date: Date = Date()
var dayPlan: DayPlan? = nil
}
}
The migration plan
Finally, I created the migration plan:
enum MigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self) { context in
// willMigrate, only access to old models
} didMigrate: { context in
// didMigrate, only access to new models
let shifts = try context.fetch(FetchDescriptor<SchemaV2.CDShift>())
for shift in shifts {
let dayPlan = DayPlan(date: shift.date)
dayPlan.shifts?.append(shift)
context.insert(dayPlan)
}
}
static var stages: [MigrationStage] {
print("MigrationPlan | stages called")
return [migrateV1toV2]
}
}
The ModelContainer
Last, but not least, how the model container is created in the App
:
struct MyApp: App {
private let container: ModelContainer
init() {
container = ModelContainer.appContainer
}
var body: some Scene {
WindowGroup {
...
}
.modelContainer(container)
}
}
This is the extension of ModelContainer
:
extension ModelContainer {
static var appContainer: ModelContainer {
let schema = Schema([
CDShift.self,
DayPlan.self
])
let modelConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: Ecosystem.current.isPreview,
groupContainer: .identifier(Ecosystem.current.appGroupIdentifier)
)
do {
// let container = try ModelContainer(for: schema, configurations: modelConfiguration)
let container = try ModelContainer(for: schema, migrationPlan: MigrationPlan.self, configurations: modelConfiguration)
AMLogger.verbose("SwiftData path: \(modelConfiguration.url.path)")
return container
} catch (let error) {
fatalError("Could not create ModelContainer: \(error)")
}
}
}
The error
This has always worked perfectly until the migration. It crashes on the fatalError
line, this is the error:
Unable to find a configuration named 'default' in the specified managed object model.
Notes
- It seems that the version of the store is never updated to 2, but it keeps staying on 1. I tried also using the lightweight migration, no crash, it seems it recognizes the new entity, but the store version is always 1.
- iCloud is enabled
- I thought that the
context
used in the custom migration blocks is not the "right" one that I use when I create my container - If I use the lightweight migration, everything seems to work fine, but I have to manually do the association between the
DayPlan
and theCDShift
objects
Do you have an idea on how to help in this case?