You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)")
        }
    }()
}

问题分析与修复方案

核心问题点

  1. 版本标识不匹配:V3版本号为2.1.1,但迁移阶段命名为migrateV2toV210,SwiftData严格按照Schema.Version的数值匹配版本,命名差异可能导致内部版本映射混乱。
  2. 迁移模型引用错误:自定义迁移中直接用V3模型读取数据,此时存储尚未完全切换到V3版本,导致模型校验失败。
  3. 错误处理缺失:try?掩盖了迁移过程中的fetch、save错误,首次启动时迁移未完成触发崩溃,重启后SwiftData自动重试完成迁移。
  4. 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属性不会影响存储版本。

额外验证步骤

  1. 清理测试设备应用数据,重新安装测试首次启动情况
  2. 开启SwiftData迁移日志,查看详细迁移过程:
    UserDefaults.standard.set(true, forKey: "com.apple.CoreData.Logging.stderr")
    UserDefaults.standard.set(true, forKey: "com.apple.CoreData.Logging.migration")
    

内容的提问来源于stack exchange,提问作者g0ld2k

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 08:42:02