SwiftData Migration Crashes on First Run

I'm having an issue with SwiftData and migrating schemas, whenever going from the V2 to V3 schema my app crashes with the following error on first launch:

The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store.

After the first launch crash, then the app runs fine, and I confirmed that the schema was upgraded as expected.

I'd like to get this resolved so that future migrations are seamless. Has anyone run into this or have any recommendations to resolve this?

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

Migration Plan

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 Creation

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

Adding Model Container to View

@main
struct Distance_TrackApp: App {
    @Environment(\.scenePhase) var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(.DistanceTrackContainer)
                .onChange(of: scenePhase) { _, phase in
                    switch phase {
                    case .background:
                        Task {
                            await WidgetUpdateService.shared.refresh()
                        }
                    default:
                        // Do Nothing
                        break
                    }
                }
        }
    }
}
Answered by _g0ld2k in 781492022

I found a solution which seems to resolve the issue overall. If the container fails to be initialized when iCloud is enabled, try initializing it without a CloudKit Database, then immediately reinitialize with the CloudKit Database. This prevents the app from crashing during migration and resolves the issue in my earlier solution that left CloudKit disabled on first run after migration.

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 = {
        
        let cloudConfig: ModelConfiguration = .init(
            Constants.CloudKitContainerName,
            groupContainer: .identifier(Constants.AppGroup),
            cloudKitDatabase: .private(Constants.CloudContainer)
        )
        
        let localConfig: ModelConfiguration = .init(
            Constants.CloudKitContainerName,
            groupContainer: .identifier(Constants.AppGroup),
            cloudKitDatabase: .none
        )
        
        do {
            let container: ModelContainer
            if let iCloudContainer = try? ModelContainer(
                for: DistanceGoal.self,
                migrationPlan: DistanceTrackMigrationPlan.self,
                configurations: cloudConfig
            ) {
                container = iCloudContainer
            } else {
                _ = try ModelContainer(
                    for: DistanceGoal.self,
                    migrationPlan: DistanceTrackMigrationPlan.self,
                    configurations: localConfig
                )
                
                container = try ModelContainer(
                    for: DistanceGoal.self,
                    migrationPlan: DistanceTrackMigrationPlan.self,
                    configurations: cloudConfig
                )
            }
            
            return container
            
        } catch {
            fatalError("Failed to configure SwiftData container. Error: \(error)")
        }
    }()
}

I found a workaround that prevents the app from crashing during migration, but it does disable iCloud syncing until the app is relaunched. I don't like this solution, I'd rather find something that doesn't disable iCloud syncing.

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 cloudConfig: ModelConfiguration = .init(
                Constants.CloudKitContainerName,
                groupContainer: .identifier(Constants.AppGroup),
                cloudKitDatabase: .private(Constants.CloudContainer)
            )
            
            let localConfig: ModelConfiguration = .init(
                Constants.CloudKitContainerName,
                groupContainer: .identifier(Constants.AppGroup),
                cloudKitDatabase: .none
            )
            
            let container: ModelContainer
            if let iCloudContainer = try? ModelContainer(
                for: DistanceGoal.self,
                migrationPlan: DistanceTrackMigrationPlan.self,
                configurations: cloudConfig
            ) {
                container = iCloudContainer
            } else {
                container = try ModelContainer(
                    for: DistanceGoal.self,
                    migrationPlan: DistanceTrackMigrationPlan.self,
                    configurations: localConfig
                )
            }
            
            return container
            
        } catch {
            fatalError("Failed to configure SwiftData container. Error: \(error)")
        }
    }()
}
Accepted Answer

I found a solution which seems to resolve the issue overall. If the container fails to be initialized when iCloud is enabled, try initializing it without a CloudKit Database, then immediately reinitialize with the CloudKit Database. This prevents the app from crashing during migration and resolves the issue in my earlier solution that left CloudKit disabled on first run after migration.

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 = {
        
        let cloudConfig: ModelConfiguration = .init(
            Constants.CloudKitContainerName,
            groupContainer: .identifier(Constants.AppGroup),
            cloudKitDatabase: .private(Constants.CloudContainer)
        )
        
        let localConfig: ModelConfiguration = .init(
            Constants.CloudKitContainerName,
            groupContainer: .identifier(Constants.AppGroup),
            cloudKitDatabase: .none
        )
        
        do {
            let container: ModelContainer
            if let iCloudContainer = try? ModelContainer(
                for: DistanceGoal.self,
                migrationPlan: DistanceTrackMigrationPlan.self,
                configurations: cloudConfig
            ) {
                container = iCloudContainer
            } else {
                _ = try ModelContainer(
                    for: DistanceGoal.self,
                    migrationPlan: DistanceTrackMigrationPlan.self,
                    configurations: localConfig
                )
                
                container = try ModelContainer(
                    for: DistanceGoal.self,
                    migrationPlan: DistanceTrackMigrationPlan.self,
                    configurations: cloudConfig
                )
            }
            
            return container
            
        } catch {
            fatalError("Failed to configure SwiftData container. Error: \(error)")
        }
    }()
}
SwiftData Migration Crashes on First Run
 
 
Q