Migrating Your Schema
Schema migration has been a part of SwiftData since it was first introduced back in iOS 17. Migrating the model schemas for your app between versions ensures that data stored in the old format can work with the expected format in the new version of your app. In this segment of the lesson, the focus will be on the high level concepts of migrations, and how you need to update your code to deal with model inheritance. Further details on migration are out of the scope of this lesson.
Setting up a Model Schema
The .modelContainer(for:) method is a convenient way to specify which models SwiftData should care about, and uses convenient defaults when setting up the container. However, it’s not the only way to set up a model container. You can declare the container as a property of the main App struct:
let modelContainer: ModelContainer = {
//....
}
You can then define your models:
let modelContainer: ModelContainer = {
let schema = Schema([Recipe.self])
}
You can even configure the container how you want:
let modelContainer: ModelContainer = {
let schema = Schema([Recipe.self])
let config = ModelConfiguration(
schema: schema,
url: URL.documentsDirectory.appending(path: "myRecipeDatabase.sqlite"),
allowsSave: true,
isAutosaveEnabled: true,
isUndoEnabled: false
)
}
Finally, you can make and return the container:
let modelContainer: ModelContainer = {
do {
let schema = Schema([Recipe.self])
let config = ModelConfiguration(
schema: schema,
url: URL.documentsDirectory.appending(path: "myRecipeDatabase.sqlite"),
allowsSave: true,
isAutosaveEnabled: true,
isUndoEnabled: false
)
// Initialize the container with the configuration
container = try ModelContainer(for: schema, configurations: [config])
} catch {
fatalError("Failed to configure SwiftData container: \(error)")
}
}
This works well for a single schema version, but what if you are introducing a new version and have to migrate?
Setting up a Model Schema with Migration
In order to make your app future proof, you should probably assume that you’ll need a schema migration at some point in the future. To help with this, you can declare your schema as a VersionedSchema:
enum RecipesSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version { Schema.Version(1, 0, 0) }
static var models: [any PersistentModel.Type] {
[Recipe.self]
}
}
This sets the version for the schema to 1.0.0, and lists the models, which in this case is just the Recipe model.
A SchemaMigrationPlan can be composed from the available schema. The enum looks like this to start:
enum RecipesMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
var currentSchemas: [any VersionedSchema.Type] =
[RecipesSchemaV1.self]
return currentSchemas
}
static var stages: [MigrationStage] {
var currentStages: [MigrationStage] = []
return currentStages
}
}
Migration When Using Child Models
Recall that model inheritance is new in iOS 26, so that means you can only handle migrations with child classes in iOS 26 and above. This means that you need to instruct the compiler that certain parts of the schema migration code are only available in certain versions of the operating system. For example:
@available(iOS 26, *)
enum RecipesSchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version { Schema.Version(2, 0, 0) }
static var models: [any PersistentModel.Type] {
[
Recipe.self,
BakedGood.self,
Beverage.self
]
}
}
The @available(iOS 26, *) code specifies that this schema is only valid on iOS 26 or above. Similarly, when building your schema migration plan, you can only add this schema when it is available.
//....
if #available(iOS 26, *) {
currentSchemas.append(RecipesSchemaV2.self)
}
//...
The schema migration plan now looks like this:
enum RecipesMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
var currentSchemas: [any VersionedSchema.Type] =
[RecipesSchemaV1.self]
if #available(iOS 26, *) {
currentSchemas.append(RecipesSchemaV2.self)
}
return currentSchemas
}
@available(iOS 26, *)
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: RecipesSchemaV1.self,
toVersion: RecipesSchemaV2.self
)
static var stages: [MigrationStage] {
var currentStages: [MigrationStage] = []
if #available(iOS 26, *) {
currentStages.append(migrateV1toV2)
}
return currentStages
}
}
The new v2 schema has been added to the schemas property definition. Also, a MigrationStage to go from version 1.0.0 to 2.0.0 has been added, and that stage has been added to the list of available stages in the stages property definition.
In the next segment, you’ll see how you can implement this in a demo.