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