SwiftData从V2迁移到V3首次启动崩溃问题求助
SwiftData V2到V3迁移首次启动崩溃问题解决建议
问题描述
从V2 Schema迁移到V3 Schema时,应用首次启动崩溃,报错:
The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store.
首次崩溃后重启应用可正常运行,Schema已完成升级,需要实现无缝迁移。
相关代码片段
V1 Schema
public enum DistanceTrackSchemaV1: VersionedSchema { public static var versionIdentifier = Schema.Version(1, 0, 0) public static var models: [any PersistentModel.Type] { [DistanceTrackSchemaV1.DistanceGoal.self] } @Model public final class DistanceGoal: Sendable { // swiftformat:disable all public let id: UUID = UUID() public var title: String = "" // swiftformat:disable all public var startingDate: Date = Date.distantPast // swiftformat:disable all public var endingDate: Date = Date.distantFuture public var distance: Double = -100 public var distanceSoFar: Double = 0 @Transient public var workouts: [DistanceWorkout] = [] @Transient public var shouldRefresh: Bool = false public init( title: String, startingDate: Date, endingDate: Date, distance: Double ) { self.title = title self.startingDate = startingDate self.endingDate = endingDate self.distance = distance } } }
V2 Schema
public enum DistanceTrackSchemaV2: VersionedSchema { public static var versionIdentifier = Schema.Version(2, 0, 0) public static var models: [any PersistentModel.Type] { [DistanceTrackSchemaV2.DistanceGoal.self] } @Model public final class DistanceGoal: Sendable { // swiftformat:disable all public let id: UUID = UUID() public var title: String = "" // swiftformat:disable all public var startingDate: Date = Date.distantPast // swiftformat:disable all public var endingDate: Date = Date.distantFuture public var distance: Double = -100 public var distanceSoFar: Double = 0 public var unit: String = DistanceMetric.miles.rawValue @Transient public var workouts: [DistanceWorkout] = [] @Transient public var shouldRefresh: Bool = false public init( title: String, startingDate: Date, endingDate: Date, unit: DistanceMetric, distance: Double ) { self.title = title self.startingDate = startingDate self.endingDate = endingDate self.unit = unit.rawValue self.distance = distance } } }
V3 Schema
public enum DistanceTrackSchemaV3: VersionedSchema { public static var versionIdentifier = Schema.Version(2, 1, 1) public static var models: [any PersistentModel.Type] { [DistanceTrackSchemaV3.DistanceGoal.self] } @Model public final class DistanceGoal: Sendable { // swiftformat:disable all public let id: UUID = UUID() public var title: String = "" // swiftformat:disable all public var startingDate: Date = Date.distantPast // swiftformat:disable all public var endingDate: Date = Date.distantFuture public var distance: Double = -100 public var distanceSoFar: Double = 0 public var unit: String = DistanceMetric.miles.rawValue public var rawWorkoutTypes: [String] = [ WorkoutType.walking.rawValue, WorkoutType.running.rawValue, WorkoutType.wheelchairRunPace.rawValue, WorkoutType.wheelchairWalkPace.rawValue, ] @Transient public var workouts: [DistanceWorkout] = [] public init( title: String, startingDate: Date, endingDate: Date, unit: DistanceMetric, distance: Double, workoutTypes: [WorkoutType] ) { self.title = title self.startingDate = startingDate self.endingDate = endingDate self.unit = unit.rawValue self.distance = distance self.rawWorkoutTypes = workoutTypes.map({$0.rawValue}) } } }
迁移计划
public typealias DistanceSchema = DistanceTrackSchemaV3 public typealias DistanceGoal = DistanceSchema.DistanceGoal enum DistanceTrackMigrationPlan: SchemaMigrationPlan { static var schemas: [VersionedSchema.Type] { [ DistanceTrackSchemaV1.self, DistanceTrackSchemaV2.self, DistanceTrackSchemaV3.self, ] } static var stages: [MigrationStage] { [ migrateV1toV2, migrateV2toV210, ] } static let migrateV1toV2 = MigrationStage.lightweight( fromVersion: DistanceTrackSchemaV1.self, toVersion: DistanceTrackSchemaV2.self ) static let migrateV2toV210 = MigrationStage.custom( fromVersion: DistanceTrackSchemaV2.self, toVersion: DistanceTrackSchemaV3.self ) { _ in } didMigrate: { context in let goals = try? context.fetch( FetchDescriptor<DistanceTrackSchemaV3.DistanceGoal>() ) goals?.forEach { goal in goal.rawWorkoutTypes = [ WorkoutType.walking.rawValue, WorkoutType.running.rawValue, WorkoutType.wheelchairRunPace.rawValue, WorkoutType.wheelchairWalkPace.rawValue, ] } try? context.save() } }
ModelContainer 创建代码
public extension ModelContainer { private enum Constants { static var AppGroup = "group.XXXXXXXXXX" static var CloudKitContainerName = "XXXXXXXXXX" static var CloudContainer = "iCloud.XXXXXXXXXX" static var SQLFile = "XXXXXXXXXX-Shared.sqlite" } static var DistanceTrackContainer: ModelContainer = { do { let config: ModelConfiguration = .init( Constants.CloudKitContainerName, groupContainer: .identifier(Constants.AppGroup), cloudKitDatabase: .private(Constants.CloudContainer) ) let container = try ModelContainer( for: DistanceGoal.self, migrationPlan: DistanceTrackMigrationPlan.self, configurations: config ) return container } catch { fatalError("Failed to configure SwiftData container. Error: \(error)") } }() }
问题分析与修复方案
核心问题点
- 版本标识不匹配:V3版本号为
2.1.1,但迁移阶段命名为migrateV2toV210,SwiftData严格按照Schema.Version的数值匹配版本,命名差异可能导致内部版本映射混乱。 - 迁移模型引用错误:自定义迁移中直接用V3模型读取数据,此时存储尚未完全切换到V3版本,导致模型校验失败。
- 错误处理缺失:
try?掩盖了迁移过程中的fetch、save错误,首次启动时迁移未完成触发崩溃,重启后SwiftData自动重试完成迁移。 - CloudKit同步竞态:启用CloudKit同步后,本地存储与云端版本同步的竞态条件可能导致模型版本校验失败。
具体修复步骤
1. 统一版本标识
修改迁移阶段命名,与版本号对应:
static let migrateV2toV3 = MigrationStage.custom( fromVersion: DistanceTrackSchemaV2.self, toVersion: DistanceTrackSchemaV3.self ) { _ in } didMigrate: { context in // 迁移逻辑 } // 更新stages数组 static var stages: [MigrationStage] { [ migrateV1toV2, migrateV2toV3, ] }
2. 修正迁移数据读取逻辑
在didMigrate中先读取V2旧数据,再映射到V3模型:
didMigrate: { context in do { let oldGoals = try context.fetch(FetchDescriptor<DistanceTrackSchemaV2.DistanceGoal>()) for oldGoal in oldGoals { if let newGoal = try context.fetch(FetchDescriptor<DistanceTrackSchemaV3.DistanceGoal>(predicate: #Predicate { $0.id == oldGoal.id })).first { newGoal.rawWorkoutTypes = [ WorkoutType.walking.rawValue, WorkoutType.running.rawValue, WorkoutType.wheelchairRunPace.rawValue, WorkoutType.wheelchairWalkPace.rawValue, ] } } try context.save() } catch { fatalError("V2 to V3 migration failed: \(error.localizedDescription)") } }
3. 完善错误处理
移除try?,改用do-catch捕获迁移错误,避免掩盖问题。
4. 处理CloudKit同步竞态
迁移完成前禁用CloudKit自动同步,完成后再启用:
static var DistanceTrackContainer: ModelContainer = { do { var config: ModelConfiguration = .init( Constants.CloudKitContainerName, groupContainer: .identifier(Constants.AppGroup), cloudKitDatabase: .private(Constants.CloudContainer) ) // 迁移前禁用同步 config.cloudKitSyncEnabled = false let container = try ModelContainer( for: DistanceGoal.self, migrationPlan: DistanceTrackMigrationPlan.self, configurations: config ) // 迁移完成后启用同步 container.cloudKitSyncEnabled = true return container } catch { fatalError("Failed to configure SwiftData container. Error: \(error)") } }()
5. 验证模型版本哈希
确保@Model类的持久化属性无变更遗漏,@Transient属性不会影响存储版本。
额外验证步骤
- 清理测试设备应用数据,重新安装测试首次启动情况
- 开启SwiftData迁移日志,查看详细迁移过程:
UserDefaults.standard.set(true, forKey: "com.apple.CoreData.Logging.stderr") UserDefaults.standard.set(true, forKey: "com.apple.CoreData.Logging.migration")
内容的提问来源于stack exchange,提问作者g0ld2k
相关产品推荐
相关产品推荐

