The reason of the error is that your new data model, after you made the changes, isn't compatible with your existing data. To handle the kind of issue, there are two options:
-
Remove the existing data store, use the new data model to create a new store, and continue the development.
-
Migrate the existing data store to the new data model.
In the development phase when my data model isn't stable, I typically go with option 1. By choosing this option, the existing data is lost, but that doesn't matter because the app is still under development.
If the app has been shipped, option 2 is the only choice, and you can implement it by versioning your models with VersionedSchema and SchemaMigrationPlan.
As an example, assuming that I have a SwiftData model named Item
at the very beginning (version 1) and I would like to evolve the model to version 2 by:
- Changing
Item
by adding a new attribute. - Adding a new model named
Item2
.
Here is the way to define the models in my version 2 app:
typealias Item = ItemSchemaV2.Item
typealias Item2 = ItemSchemaV2.Item2
enum ItemSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version {
return Schema.Version(1, 0, 0) //"ItemSchemaV1"
}
static var models: [any PersistentModel.Type] {
[Item.self]
}
@Model
final class Item {
var timestamp: Date = Date.now
init(timestamp: Date = .now) {
self.timestamp = timestamp
}
}
}
enum ItemSchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version {
return Schema.Version(2, 0, 0) //"ItemSchemaV2"
}
static var models: [any PersistentModel.Type] {
[Item.self, Item2.self]
}
@Model
final class Item {
var timestamp: Date = Date.now
var title: String = ""
init(timestamp: Date = .now, title: String = "") {
self.timestamp = timestamp
self.title = title
}
}
@Model
final class Item2 {
var transformedTimestamp: String
init(transformedTimestamp: String = "") {
self.transformedTimestamp = transformedTimestamp
}
}
}
enum ItemsMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[ItemSchemaV1.self, ItemSchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: ItemSchemaV1.self, toVersion: ItemSchemaV2.self)
}
The above code uses lightweight migration stage (.lightweight(fromVersion:toVersion:)) because both of the changes are supported by lightweight migration.
In the case where your change is not supported by lightweight migration, you can use a custom migration stage (custom(fromVersion:toVersion:willMigrate:didMigrate:)). The following code example uses a custom migration stage to set Item.title
to "new title" in the didMigrate
closure:
//static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: ItemSchemaV1.self, toVersion: ItemSchemaV2.self)
static let migrateV1toV2 = MigrationStage.custom(fromVersion: ItemSchemaV1.self,
toVersion: ItemSchemaV2.self) { context in
print("willMigrate: No preprocess needed.")
} didMigrate: { context in
print("didMigrate. Initialize the title after the migration.")
if let items = try? context.fetch(FetchDescriptor<ItemSchemaV2.Item>()) {
for item in items {
item.title = "new title"
}
try? context.save()
}
}
Best,
——
Ziqiao Chen
Worldwide Developer Relations.