4

I'm having an issue with SwiftData and my migration, whenever going from the V2 to V3 schema the 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?

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

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)")
        }
    }()
}
2
  • What is the point of setting the value of rawWorkoutTypes in the migration to the same value it has as a default value? Commented Feb 27, 2024 at 12:13
  • That was one of my attempts to resolve my migration problem, it had no impact Commented Feb 27, 2024 at 15:48

1 Answer 1

2

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)")
        }
    }()
}
Sign up to request clarification or add additional context in comments.

1 Comment

This was useful also to "solve" my issue with custom migration, but I noticed that the version of the store is not updated to the version of the new versioned schema, but it keeps staying on "1.0.0". Is this something you also experienced?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.