SwiftData迁移至V2版本添加枚举字段触发致命错误
SwiftData V1→V2迁移崩溃问题解决
问题触发原因
迁移时抛出Fatal error: Expected only Arrays for Relationships - EnumJ,是因为SwiftData在解析V2模型时,误将新增的Codable枚举字段识别为Relationship类型,导致关系类型校验失败。
修复步骤
1. 规范枚举类型的Codable实现
显式实现枚举的Codable逻辑,避免SwiftData隐式解析时的类型混淆:
enum EnumJ: String, Codable, CaseIterable { case caseA, caseB, caseC func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(rawValue) } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try container.decode(String.self) guard let validCase = EnumJ(rawValue: rawValue) else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Invalid EnumJ value: \(rawValue)" ) } self = validCase } }
2. 明确V2模型的字段类型声明
在V2的VersionedSchema中,必须显式标记枚举字段的类型,禁止依赖自动推断:
enum SchemaV2: VersionedSchema { static var versionIdentifier: Schema.Version = .init(2, 0, 0) static var models: [any PersistentModel.Type] = [ModelA.self, ModelB.self] @Model class ModelA { // 修改原有字段类型(示例:V1为String,V2改为Int) var oldField: Int // 显式声明枚举字段类型 var newEnumField: EnumJ // 修改关系的deleteRule @Relationship(deleteRule: .cascade) var relatedItems: [ModelB]? init(oldField: Int, newEnumField: EnumJ) { self.oldField = oldField self.newEnumField = newEnumField } } @Model class ModelB { // 对应修改的字段与关系 var id: UUID @Relationship(inverse: \ModelA.relatedItems) var parent: ModelA? init(id: UUID) { self.id = id } } }
3. 修正自定义迁移逻辑
在迁移阶段明确处理字段转换与枚举默认值,避免类型歧义:
struct CustomMigrationV1ToV2: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] = [SchemaV1.self, SchemaV2.self] static var stages: [MigrationStage] = [ .custom( fromVersion: SchemaV1.versionIdentifier, toVersion: SchemaV2.versionIdentifier, willMigrate: { context in // 迁移前处理:转换旧字段类型 let oldModelAFetch = FetchDescriptor<SchemaV1.ModelA>() do { let oldModels = try context.fetch(oldModelAFetch) for model in oldModels { // 示例:将V1的String类型oldField转为Int兼容值 if let intValue = Int(model.oldField) { model.oldField = String(intValue) } else { model.oldField = "0" } } try context.save() } catch { print("Pre-migration error: \(error)") } }, didMigrate: { context in // 迁移后处理:为新增枚举字段设置默认值 let newModelAFetch = FetchDescriptor<SchemaV2.ModelA>() do { let newModels = try context.fetch(newModelAFetch) for model in newModels { // 若枚举字段为可选类型,补全默认值 if model.newEnumField == nil { model.newEnumField = .caseA } } try context.save() } catch { print("Post-migration error: \(error)") } } ) ] }
4. 验证容器配置
确保ModelContainer正确关联迁移计划,无重复定义:
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer( for: [ModelA.self, ModelB.self], migrationPlan: CustomMigrationV1ToV2.self ) } }
额外注意事项
- 避免枚举字段名与关系属性名重复,防止SwiftData解析混淆
- 所有模型变更(字段类型、关系规则)必须在VersionedSchema中显式声明
- 迁移时确保数据转换的兼容性,避免丢失或损坏原有数据
内容的提问来源于stack exchange,提问作者Tim
相关产品推荐
相关产品推荐

