SwiftData自定义迁移仅在iPad生效,iPhone及模拟器被跳过问题排查
问题描述
为包含自定义枚举和结构体(复合属性)的SwiftData数据模型执行自定义迁移,新版本新增若干枚举。但迁移仅在iPad上成功执行,iPhone及模拟器中被跳过——即便从Xcode或TestFlight安装的是同一版本。迁移被跳过时,新增枚举未完成初始化(尽管已设置默认值),触发如下错误:
CoreData: warning: validation recovery attempt FAILED with Error Domain=NSCocoaErrorDomain Code=1560 "Multiple validation errors occurred." UserInfo={NSDetailedErrors=(
"Error Domain=NSCocoaErrorDomain Code=1570 "%{PROPERTY}@ is a required value."
已完成的排查:
- 确认DataModels的
versionChecksums中V1版本校验码始终一致; - 通过断点验证
MigrationPlan在所有设备及模拟器中均已加载,但仅在iPad上运行; - 测试环境为iOS17.4和17.5。
相关代码片段
MigrationPlan
enum DataMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [DataSchemaV1.self, DataSchemaV2.self] } static let migrateV1toV2 = MigrationStage.custom( fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self, willMigrate: { context in print("will migrate") }, didMigrate: { context in print("did migrate") // … setting values for new properties } ) static var stages: [MigrationStage] { [migrateV1toV2] } }
ModelContainer
let modelConfiguration = ModelConfiguration(isStoredInMemoryOnly: false) sharedModelContainer = try ModelContainer ( for: DataSchemaV2.FilterGroup.self, migrationPlan: DataMigrationPlan.self, configurations: modelConfiguration )
DataSchemaV1
enum DataSchemaV1: VersionedSchema { static var versionIdentifier = Schema.Version(1, 0, 0) static var models: [any PersistentModel.Type] { [DataSchemaV1.FilterGroup.self] } @Model final class FilterGroup: Identifiable { public let id: String = UUID().uuidString var timestamp: Date = Date.now var theme: WidgetTheme = WidgetTheme.calendar // string enum var myStruct: [MyStruct] = [] } static var sampleData: FilterGroup { return FilterGroup(...) } struct MyStruct: Codable, Identifiable { var id: UUID = UUID() var someEnum: SomeEnum = SomeEnum.something ... } }
DataSchemaV2
enum DataSchemaV2: VersionedSchema { static var versionIdentifier = Schema.Version(2, 0, 0) static var models: [any PersistentModel.Type] { [DataSchemaV2.FilterGroup.self] } @Model final class FilterGroup: Identifiable { // ... same as v1 var newlyAddedEnum: NewEnum = NewEnum.something } // ... }
原因分析及解决方案
可能原因
- 自动迁移判定差异:CoreData在iPhone/模拟器上可能判定当前schema与持久化存储的schema兼容,跳过自定义迁移直接尝试自动迁移,但自动迁移无法正确处理自定义枚举的默认值初始化,导致校验失败。
- 设备特定缓存:iOS17.x的SwiftData在不同设备上存在schema校验缓存差异,导致iPhone/模拟器未正确识别schema版本变化,跳过迁移流程。
解决方案
禁用自动迁移,强制自定义迁移
在ModelConfiguration中明确关闭自动迁移,确保自定义迁移计划被强制执行:let modelConfiguration = ModelConfiguration( isStoredInMemoryOnly: false, automaticMigrationsEnabled: false ) sharedModelContainer = try ModelContainer ( for: DataSchemaV2.FilterGroup.self, migrationPlan: DataMigrationPlan.self, configurations: modelConfiguration )确保枚举类型的CoreData兼容性
检查新增的NewEnum是否正确遵循Codable和PersistentEnum(SwiftData专属枚举协议),并确保原始类型与CoreData存储兼容:enum NewEnum: String, Codable, PersistentEnum { case something // 其他枚举值 }在自定义迁移中显式设置枚举值
即便属性有默认值,也在didMigrate闭包中为所有现有实体显式设置新增枚举值,避免自动初始化失败:didMigrate: { context in print("did migrate") do { let filterGroups = try context.fetch(FetchDescriptor<DataSchemaV2.FilterGroup>()) for group in filterGroups { group.newlyAddedEnum = NewEnum.something } try context.save() } catch { print("Migration save failed: \(error)") } }清理设备缓存
模拟器侧:彻底删除App并重置模拟器;iPhone侧:删除App后重启设备,清除可能存在的schema缓存。
内容的提问来源于stack exchange,提问作者ARS

