How to migrate to a new schema with SwiftData in iOS
Learn how to create a migration plan in SwiftData.
20 Nov 2023 · 4 min read
Whenever we release a new version of our app where we made changes to our SwiftData models, we need to make sure that SwiftData is able to migrate between the old and the new schema to prevent data loss.
Let's directly jump in and how to create a migration plan in SwiftData.

The basic steps of a migration with SwiftData are:
- Use VersionedSchema to create versions of SwiftData models
- Use SchemaMigrationPlan to order the created versions
- Define a migration stage for each migration - which can be lightweight or custom
Let's look at each step in more detail.
1. Use VersionedSchema to create versions of SwiftData models
If we want to make a change to a SwiftData model, the first thing to do is to create a schema version for the old model. For example:
enum UserSchemaV1: VersionedSchema {static var versionIdentifier: String? = "V1"static var models: [any PersistentModel.Type] {return [User.self]}@Modelclass User {@Attribute(.unique) let id: Stringlet validated: Bool...}}enum UserSchemaV2: VersionedSchema {...@Modelclass User {@Attribute(.unique) let id: String@Attribute(originalName: "validated") let isValidated: Bool}}
The above example shows two schema versions each containing the model classes they define. In the second version, we renamed the validated property to isValidated and marked it with an appropriate attribute. With that in place, SwiftData can migrate those two versions with a lightweight migration.
2. Use SchemaMigrationPlan to order the created versions
After defining a new version, we need to create a migration plan which describes how to handle the migrations from version to version:
enum UsersMigrationPlan: SchemaMigrationPlan {static var schemas: [any VersionedSchema.Type] {[UserSchemaV1.self, UserSchemaV2.self]}}
With schemas, we tell the migration plan in which order the migration should be performed.
3. Define the migration stage for each migration
With that in place, we now can define the migration stage for each migration. A migration stage can be a lightweight or custom.
For a specific set of changes, SwiftData can perform a lightweight migration. Examples of those changes are adding, renaming or deleting entities, attributes or relationships, changing the relationship type and more.
Since we only renamed an attribute in our example and already annotated it, we can use the lightweight migration:
enum UsersMigrationPlan: SchemaMigrationPlan {static var schemas: [any VersionedSchema.Type] {[UserSchemaV1.self, UserSchemaV2.self]}static var stages: [MigrationStage] {[migrateV1toV2]}static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: UserSchemaV1.self,toVersion: UserSchemaV2.self)}
When changes exceed the capabilities of a lightweight migration, we need to do a custom migration which can look as follows:
static let migrateV1toV2 = MigrationStage.custom(fromVersion: UserSchemaV1.self,toVersion: UserSchemaV2.self,willMigrate: { context in// Custom steps before the migration begins},didMigrate: { context in// Custom steps after the migration})
As we can see above, the custom stage provides us with two closures, which we can use to perform an operation before or after the data is migrated.

Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips