I came across this issue myself. I used a Codable struct in one of my models.
If you are still in development the easiest solution would be to delete the SqlLite database and start from scratch. But if your app is already out there and the user have data stored you need to migrate it.
SwiftData stores your Codable struct as encoded value. So every attempt to decode it back will fail as soon as you change any property of the struct.
You will need to use 2 Migrations to change the initial struct. We can only make changes to the entities before and after the Migration is done. SwiftData applies the changes to the underlying SQLite storage by itself. We cannot control how this is done.
In the first Migration we will create another property and map the data of TargetData to it. In the second migration we drop the old property. From now on you need to use the new property. We cannot name it the same as the old name, Migration will fail if we do so.
// to access the Items in your other code more simple use a type alias
// this will point to the newest version of CloudInterval and TargetData
typealias CloudInterval = DataSchemaV3.CloudInterval
typealias TargetData = DataSchemaV3.TargetData
enum DataSchemaV1: VersionedSchema{
static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Self.CloudInterval.self] }
@Model
class CloudInterval {
// ... other properties
// specify the type explicitly to avoid confusion
var targetData: DataSchemaV1.TargetData = TargetData()
init(){}
}
struct TargetData: Codable, Equatable {
var minHeartRate: Int = 0
var maxHeartRate: Int = 0
var speed: Double = 0
var time: TimeInterval = 0
var distance: Double = 0
var enableTargetSpeed = false
var enableTargetHeartRate = false
var enableTargetTime = false
var speedTolerance: Double = 0.02
}
}
enum DataSchemaV2: VersionedSchema{
static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] { [Self.CloudInterval.self] }
@Model
class CloudInterval {
// ... other properties
// specify the type explicitly to avoid confusion
var targetData: DataSchemaV1.TargetData = DataSchemaV1.TargetData()
// we create a property with a different name that will hold our new values
var newTargetData: DataSchemaV3.TargetData = DataSchemaV3.TargetData()
init(){}
}
}
enum DataSchemaV3: VersionedSchema{
static var versionIdentifier: Schema.Version = Schema.Version(3, 0, 0)
static var models: [any PersistentModel.Type] { [Self.CloudInterval.self] }
@Model
class CloudInterval {
// ... other properties
// we now have only the new created property here
var newTargetData: DataSchemaV3.TargetData = DataSchemaV3.TargetData()
init(){}
}
struct TargetData: Codable, Equatable {
var minHeartRate: Int = 0
var maxHeartRate: Int = 0
var speed: Double = 0
var time: TimeInterval = 0
var distance: Double = 0
var enableTargetSpeed = false
var enableTargetHeartRate = false
var enableTargetTime = false
var speedTolerance: Double = 0.02
// added properties here
var integerValue: Int = 0
var booleanValue: Bool = false
}
}
enum DataMigrationPlan: SchemaMigrationPlan{
// here we add the types involved in the migration
static var schemas: [any VersionedSchema.Type] { [DataSchemaV1.self, DataSchemaV2.self, DataSchemaV3.self]}
// return the stages that should be run during migration
static var stages: [MigrationStage] { [migrationV1ToV2, migrationV2ToV3] }
static let migrationV1ToV2 = MigrationStage.custom(fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self, willMigrate: nil) { context in
// this will run after the migration completed and we now have V2 items
let v2CloudIntervals = try context.fetch(FetchDescriptor<DataSchemaV2.CloudInterval>())
v2CloudIntervals.forEach{ v2CloidInterval in // initialize the new TargetData here and assign the values
// from the old TargetData
v2CloidInterval.newTargetData = DataSchemaV3.TargetData()
}
// save changes to the context
try context.save()
}
// with this migration we drop the old targetData property
static let migrationV2ToV3 = MigrationStage.lightweight(fromVersion: DataSchemaV2.self, toVersion: DataSchemaV3.self)
}
The only thing left to do would be to use the migration while creating the container
do {
let container = try ModelContainer(for: schema, migrationPlan: DataMigrationPlan.self, configurations: [modelConfiguration])
return container
} catch {
fatalError("Could not create ModelContainer: \(error)")
}