Hi, I'm trying to make some changes to my SwiftData model and I want to add a new non-optional property to one of my model classes.
My current model was not part of a VersionedSchema so I first encapsulated it into one
public enum FeynnDataModelsSchemaV1: VersionedSchema {
public static var versionIdentifier: Schema.Version = .init(1, 0, 0)
public static var models: [any PersistentModel.Type] {
[FeynnDataModelsSchemaV1.Workout.self, FeynnDataModelsSchemaV1.Activity.self, FeynnDataModelsSchemaV1.ActivityRecord.self, FeynnDataModelsSchemaV1.WorkoutSession.self]
}
}
Then I run the app and everything works as expected.
Secondly, I create the V2 and add the new property:
public enum FeynnDataModelsSchemaV2: VersionedSchema {
public static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)
public static var models: [any PersistentModel.Type] {
[FeynnDataModelsSchemaV2.Workout.self, FeynnDataModelsSchemaV2.Activity.self, FeynnDataModelsSchemaV2.ActivityRecord.self, FeynnDataModelsSchemaV2.WorkoutSession.self]
}
}
extension FeynnDataModelsSchemaV2 {
@Model
final public class Activity: Hashable {
...
public var activityType: ActivityType = ActivityType.traditionalStrengthTraining
...
}
}
Lastly, I create the schema migration plan and add it to my modelContainer:
public enum FeynnDataModelsMigrationPlan: SchemaMigrationPlan {
public static var schemas: [VersionedSchema.Type] = [
FeynnDataModelsSchemaV1.self,
FeynnDataModelsSchemaV2.self
]
public static var stages: [MigrationStage] = [migrateV1toV2]
public static var migrateV1toV2 = MigrationStage.custom(fromVersion: FeynnDataModelsSchemaV1.self, toVersion: FeynnDataModelsSchemaV2.self, willMigrate: {moc in
let activities = try? moc.fetch(FetchDescriptor<Activity>())
print("\(activities?.count ?? 919191991)")
}, didMigrate: {moc in
let activities = try? moc.fetch(FetchDescriptor<Activity>())
print("\(activities?.count ?? 88888888)")
activities?.forEach { activity in activity.activityType = .traditionalStrengthTraining }
try? moc.save()
})
}
if let container = try? ModelContainer(
for: Workout.self, Activity.self, ActivityRecord.self, WorkoutSession.self,
migrationPlan: FeynnDataModelsMigrationPlan.self,
configurations: ModelConfiguration(cloudKitDatabase: .automatic)) {
self.container = container
} else {
self.container = try ModelContainer(
for: Workout.self, Activity.self, ActivityRecord.self, WorkoutSession.self,
migrationPlan: FeynnDataModelsMigrationPlan.self,
configurations: ModelConfiguration(cloudKitDatabase: .none))
}
After running this, the application runs as expected, but as soon as I render a view that references Activity.activityType
the app crashes trying to get the value for my existing activities given that it is nil, pointing out that the migration stage was not ran?
None of the print statements in the didMigrate
or willMigrate
can be found within the logs either.
I have tried several approaches creating more VersionedSchemas and I run into more issues such as Cannot use stuck migration with an unknown model version
.
I feel like the current way of handling schema versions of SwiftData is very confusing and opaque for the developer to know what is going on. I don't know if the underlying database is picking the un-versioned schema, the V1 or the V2.
Is there anything I'm doing wrong? What can I do apart from making the new property optional and having a computed property that unwraps it giving it a default value when nil?
Thank you!