This demo is going to be performed in stages. Since you are working with code for schema migration, you need two versions, one to migrate from and another for the final version of the schema.
There is a note however: as of beta 7, SwiftData does not properly migrate a model schema that has a parent class to a schema that has that parent class and children of that class. This demo will cover what the code should eventually do, and we’ll have to hope that this is fixed by the time of the final release later this year.
Setup the first version
To setup the first schema model, open the starter project for this lesson, which is mostly a copy of the final project from Lesson 1, which dealt with Recipe model types only. The one change is that the NavigationStack is now in the body for ContentView instead of inside the #Preview block. This ensures the NavigationStack is shown in the simulator.
Open the project, and open the AppMain.swift file.
There was a model schema already present in this project, but to handle migration, it needs to be a VersionedSchema instead. Above the App in AppMain.swift, add the following code for version 1 of the schema:
enum RecipesSchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version { Schema.Version(1, 0, 0) }
static var models: [any PersistentModel.Type] {
[Recipe.self, Ingredient.self]
}
}
This code does 2 things. First, it sets the versionIdentifier as 1.0.0, allowing one to distinguish it from versions in the future. The code also lists the PersistentModels for this version, which in this case are Recipe and Ingredient.
In AppMain, create a ModelContainer that uses this schema above the body property:
let modelContainer: ModelContainer = {
var container: ModelContainer
do {
let schema = Schema(versionedSchema: RecipesSchemaV1.self)
container = try ModelContainer(for: schema)
return container
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
This ModelContainer is defined in-line, and uses the VersionedSchema added earlier. That schema is then used to construct the container.
Finally, use this container as the argument to the modelContainer modifier:
ContentView()
.modelContainer(modelContainer)
Finally, for this example, let’s borrow the existing plus button in the toolbar to add sample data to the database. In ContentView, go to the addRecipe function and replace the code inside the withAnimation block with the following:
for recipe in Recipe.sampleData {
modelContext.insert(recipe)
}
With this change, when you press the button, basic Recipe model objects will be added to the database.
Functionally, the app should behave the same as the final project from lesson 1, although there you saw it in the preview canvas. You’ll test this functionality later during the migration.
Setup the final version
To setup the second schema model, open up the intermediate project for this lesson. This is the same as the final project from lesson 3, except it also has the VersionedSchema changes we made to the starter project earlier.
The key change to make here is adding the new schema, and also the schema migration plan, to AppMain.swift. Start with the new schema, which has an updated version number and the additional models.
@available(iOS 26, *)
enum RecipesSchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version { Schema.Version(2, 0, 0) }
static var models: [any PersistentModel.Type] {
[
Recipe.self,
Ingredient.self,
BakedGood.self,
Beverage.self
]
}
}
Next, define the schema migration plan. This comes with a few parts. First, define the schemas that are in play:
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
}
Remember, the version 2 schema is only available on iOS 26 or above, so an availability check is made before appending that schema to the schema list. Next, define the migration stage to take you from version 1 to version 2, and add it to the stages property:
@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
}
}
Again, the migration from version 1 to version 2 can only happen on iOS 26 or above, so the stage, and the inclusion of that stage in the list of migration stages, is gated by the availability check.
Perform the migration
Up to now, this module has worked inside the Xcode Canvas. For this migration example, the simulator will be used instead. I have the iPhone16 Pro simulator up on the right side of the screen, and there isn’t a version of SwiftRecipes installed yet. Open the starter project, and build and run for this simulator, which loads the app with the version 1 schema onto the simulator.
Now exercise the app. You can select a recipe, see the detail view, and scroll through the list.
Now, let’s migrate the app. Switch to the intermediate project, and build and run for this simulator. This will load the app with version 2 of the schema over the app with version 1. When the app starts, it should migrate the data to version 2 of the schema.
However, an error is thrown here, which relates to an issue when using the @Unique attribute. If that is commented out in the Recipe class and the migration is attempted again, it still fails. This is because the compiler, for some reason, is ignoring the version identifier for the schemas (something that has been noted by Apple Developers in the developer forums), and not recognizing the 2 schemas as different, even with the addition of the child models and their own properties.
If things worked as intended, the new migrated model would include the new plannedDate property, and the new Beverage and BakedGood child models.
So, the current state of migration in iOS and Xcode 26 Beta 7 is not good. Hopefully this will change in a future release!
If things worked as intended, this is an example of a very simple, lightweight migration of the model schema. The nice thing about it is that the addition of the child models, as long as you are working on an iOS 26 or above system, is very easy to implement!