SwiftData migrations are a kind of issues that really feel non-obligatory… proper till you ship an replace and actual customers improve with actual knowledge on disk.
On this submit we’ll dig into:
- implement schema variations with
VersionedSchema - When you must introduce new schema variations
- When SwiftData can migrate robotically and while you’ll have to jot down handbook migrations with
SchemaMigrationPlanandMigrationStage - deal with further advanced migrations the place you want “bridge” variations
By the top of this submit you must have a reasonably stable understanding of SwiftData’s migration guidelines, potentialities, and limitations. Extra importantly: you’ll know easy methods to preserve your migration work proportional. Not each change wants a customized migration stage, however some modifications completely do.
Implementing easy variations with VersionedSchema
Each knowledge mannequin ought to have no less than one VersionedSchema. What I imply by that’s that even should you haven’t launched any mannequin updates but, your preliminary mannequin needs to be shipped utilizing a VersionedSchema.
That provides you a steady place to begin. Introducing VersionedSchema after you’ve already shipped is feasible, however there’s some threat concerned with not getting issues proper from the beginning.
On this part, I’ll present you easy methods to outline an preliminary schema, how one can reference “present” fashions cleanly, and when you must introduce new variations.
Defining your preliminary mannequin schema
When you’ve by no means labored with versioned SwiftData fashions earlier than, the nested sorts that you will see in a second can look just a little odd at first. The thought is straightforward although:
- Every schema model defines its personal set of
@Mannequinsorts, and people sorts are namespaced to that schema (for instanceExerciseSchemaV1.Train). - Your app code usually needs to work with “the present” fashions with out spelling
SchemaV5.Trainin every single place. - A
typealiasallows you to preserve your name websites clear whereas nonetheless being express about which schema model you’re utilizing.
One very sensible consequence of that is that you just’ll typically find yourself with two sorts of “fashions” in your codebase:
- Versioned fashions:
ExerciseSchemaV1.Train,ExerciseSchemaV2.Train, and so on. These exist so SwiftData can motive about schema evolution. - Present fashions:
typealias Train = ExerciseSchemaV2.Train. These exist so the remainder of your app stays readable and also you need not refactor half your code while you introduce a brand new schema model.
Each mannequin schema that you just outline will conform to the VersionedSchema protocol and include the next two fields:
versionIdentifiera semantic versioning identifier on your schemafashionsa listing of mannequin objects which might be a part of this schema
A minimal V1 → V2 instance
As an example a easy VersionedSchema definition, we’ll use a tiny Train mannequin as our V1.
In V2 we’ll add a notes area. This type of change is fairly widespread in my expertise and it is a good instance of a so-called light-weight migration as a result of current rows can merely have their notes set to nil.
import SwiftData
enum ExerciseSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Model(1, 0, 0)
static var fashions: [any PersistentModel.Type] = [Exercise.self]
@Mannequin
remaining class Train {
var identify: String
init(identify: String) {
self.identify = identify
}
}
}
enum ExerciseSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Model(2, 0, 0)
static var fashions: [any PersistentModel.Type] = [Exercise.self]
@Mannequin
remaining class Train {
var identify: String
var notes: String?
init(identify: String, notes: String? = nil) {
self.identify = identify
self.notes = notes
}
}
}
In the remainder of your app, you’ll often wish to work with the newest schema’s mannequin sorts:
typealias Train = ExerciseSchemaV2.Train
That manner you’ll be able to write Train(...) as a substitute of ExerciseSchemaV2.Train(...).
Realizing when to introduce new VersionedSchemas
Personally, I solely introduce a brand new model after I make mannequin modifications in between App Retailer releases. For instance, I will ship my app v1.0 with mannequin v1.0. After I wish to make any variety of mannequin modifications in my app model 1.1, I’ll introduce a brand new mannequin model too. Normally I will identify the mannequin model 2.0 since that simply is sensible to me. Even when I find yourself making a great deal of modifications in separate steps, I hardly ever create a couple of mannequin model for a single app replace. As we’ll see within the advanced migrations sections there may be exceptions if I would like a multi-stage migration however these are very uncommon.
So, introduce a brand new VersionedSchema while you make mannequin modifications after you have already shipped a mannequin model.
One factor that you will need to remember is that customers can have totally different migration paths. Some customers will replace to each single mannequin you launch, others will skip variations.
SwiftData handles these migrations out of the field so you do not have to fret about them which is nice. It is nonetheless good to pay attention to this although. Your mannequin ought to be capable of migrate from any outdated model to any new model.
Typically, SwiftData will determine the migration path by itself, let’s examine how that works subsequent.
Automated migration guidelines
If you outline your entire versioned schemas appropriately, SwiftData can simply migrate your knowledge from one model to a different. Typically, you would possibly wish to assist SwiftData out by offering a migration plan. I usually solely do that for my customized migrations however it’s doable to optimize your migration paths by offering migration plans for light-weight migrations too.
What “computerized migration” means in SwiftData
SwiftData can infer sure schema modifications and migrate your retailer with none customized logic. In a migration plan, that is represented as a light-weight stage.
One nuance that’s price calling out: SwiftData can carry out light-weight migrations with out you writing a SchemaMigrationPlan in any respect. However when you do undertake versioned schemas and also you need predictable, testable upgrades between shipped variations, explicitly defining levels is essentially the most easy technique to make your intent unambiguous.
I like to recommend going for each approaches (with and with out plans) no less than as soon as so you’ll be able to expertise them and you may resolve what works greatest for you. When doubtful, it by no means hurts to construct migration plans for light-weight migrations even when it is not strictly wanted.
Let’s have a look at how you’d outline a migration plan on your knowledge retailer, and the way you should use your migration plan.
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [ExerciseSchemaV1.self, ExerciseSchemaV2.self]
static var levels: [MigrationStage] = [v1ToV2]
static let v1ToV2 = MigrationStage.light-weight(
fromVersion: ExerciseSchemaV1.self,
toVersion: ExerciseSchemaV2.self
)
}
On this migration plan, we have outlined our mannequin variations, and we have created a light-weight migration stage to go from our v1 to our v2 fashions. Be aware that we technically did not should construct this migration plan as a result of we’re doing light-weight migrations solely, however for completeness sake you’ll be able to be sure to outline migration steps for each mannequin change.
If you create your container, you’ll be able to inform it to make use of your plan as follows:
typealias Train = ExerciseSchemaV2.Train
let container = strive ModelContainer(
for: Train.self,
migrationPlan: AppMigrationPlan.self
)
Realizing when a light-weight migration can be utilized
The next modifications are light-weight modifications and do not require any customized logic:
- Add an non-obligatory property (like
notes: String?) - Take away a property (knowledge is dropped)
- Make a property non-obligatory (non-optional → non-obligatory)
- Rename a property should you map the unique saved identify
These modifications don’t require SwiftData to invent new values. It might both preserve the outdated worth, transfer it, or settle for a nil the place no worth existed earlier than.
Safely renaming values
If you rename a mannequin property, the shop nonetheless incorporates the outdated attribute identify. Use @Attribute(originalName:) so SwiftData can convert from outdated property names to new ones.
@Mannequin
remaining class Train {
@Attribute(originalName: "identify")
var title: String
init(title: String) {
self.title = title
}
}
When you shouldn’t depend on light-weight migration
Light-weight migrations break down when your new schema introduces a brand new requirement that outdated knowledge cannot fulfill. Or in different phrases, if SwiftData cannot robotically decide easy methods to transfer from the outdated mannequin to the brand new one.
Some examples of mannequin modifications that may require a heavy migration are:
- Including non-optional properties and not using a default worth
- Any change that requires a change step:
- parsing / composing values
- merging or splitting entities
- altering a price’s kind
- knowledge cleanup (dedupe, normalizing strings, fixing invalid states)
When you’re making a change that SwiftData cannot migrate by itself, you are in handbook migration land and you will wish to pay shut consideration to this part.
A fast word on “defaults”
You’ll generally see recommendation like “simply add a default worth and also you’re wonderful”. That may be true, however there’s a delicate entice: a default worth in your Swift initializer doesn’t essentially imply current rows get a price throughout migration.
When you’re introducing a required area, assume you must explicitly backfill it except you’ve examined the migration from an actual on-disk retailer. That is the place handbook migrations grow to be vital.
Performing handbook migrations utilizing a migration plan
As you have seen earlier than, a migration plan lets you describe how one can migrate from one mannequin model to the following. Our instance from earlier than leveraged a light-weight migration. We’ll arrange a customized migration for this part.
We’ll stroll by a few eventualities with growing complexity so you’ll be able to ease into more durable migration paths with out being overwhelmed.
Assigning defaults for brand new, non-optional properties
State of affairs: you add a brand new required area like createdAt: Date to an current mannequin. Current rows don’t have a price for it. Emigrate this, now we have two choices
- Choice A: make the property non-obligatory and settle for “unknown”. This might permit us to make use of a light-weight migration however we’d have
nilvalues forcreatedAt - Choice B: write a handbook migration and preserve the property as non-optional
Choice B is the cleaner choice because it permits us to have a extra sturdy knowledge mannequin. Right here’s what this seems to be like while you truly wire it up. First, outline schemas the place V2 introduces our createdAt property:
import SwiftData
enum ExerciseCreatedAtSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Model(1, 0, 0)
static var fashions: [any PersistentModel.Type] = [Exercise.self]
@Mannequin
remaining class Train {
var identify: String
init(identify: String) {
self.identify = identify
}
}
}
enum ExerciseCreatedAtSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Model(2, 0, 0)
static var fashions: [any PersistentModel.Type] = [Exercise.self]
@Mannequin
remaining class Train {
var identify: String
var createdAt: Date
init(identify: String, createdAt: Date = .now) {
self.identify = identify
self.createdAt = createdAt
}
}
}
Subsequent we are able to add a customized stage that units createdAt for current rows. We’ll speak about what the willMigrate and didMigrate closure are in a second; let’s take a look at the migration logic first:
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [ExerciseCreatedAtSchemaV1.self, ExerciseCreatedAtSchemaV2.self]
static var levels: [MigrationStage] = [v1ToV2]
static let v1ToV2 = MigrationStage.customized(
fromVersion: ExerciseCreatedAtSchemaV1.self,
toVersion: ExerciseCreatedAtSchemaV2.self,
willMigrate: { _ in },
didMigrate: { context in
let workout routines = strive context.fetch(FetchDescriptor())
for train in workout routines {
train.createdAt = Date()
}
strive context.save()
}
)
}
With this modification, we are able to assign a wise default to createdAt. As you noticed now we have two migration levels; willMigrate and didMigrate. Let’s have a look at what these are about subsequent.
Taking a better have a look at advanced migration levels
willMigrate
willMigrate is run earlier than your schema is utilized and needs to be used to wash up your “outdated” (current) knowledge if wanted. For instance, should you’re introducing distinctive constraints you’ll be able to take away duplicates out of your unique retailer in willMigrate. Be aware that willMigrate solely has entry to your outdated knowledge retailer (the “from” mannequin). So you’ll be able to’t assign any values to your new fashions on this step. You possibly can solely clear up outdated knowledge right here.
didMigrate
After making use of your new schema, didMigrate is named. You possibly can assign your required values right here. At this level you solely have entry to your new mannequin variations.
I’ve discovered that I usually do most of my work in didMigrate, as a result of I will assign knowledge there; I do not typically have to organize my outdated knowledge for migration.
Establishing further advanced migrations
Typically you may should do migrations that reshape your knowledge. A standard case is introducing a brand new mannequin the place one of many new mannequin’s fields consists from values that was saved elsewhere.
To make this concrete, think about you began with a mannequin that shops “abstract” exercise knowledge in a single mannequin:
import SwiftData
enum WeightSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Model(1, 0, 0)
static var fashions: [any PersistentModel.Type] = [WeightData.self]
@Mannequin
remaining class WeightData {
var weight: Float
var reps: Int
var units: Int
init(weight: Float, reps: Int, units: Int) {
self.weight = weight
self.reps = reps
self.units = units
}
}
}
Now you wish to introduce PerformedSet, and have WeightData include a listing of carried out units as a substitute. You may attempt to take away weight/reps/units from WeightData in the identical model the place you add PerformedSet, however that makes migration unnecessarily onerous: you continue to want the unique values to create your first PerformedSet.
The dependable method right here is identical bridge-version technique we used earlier:
- V2 (bridge): preserve the outdated fields round underneath legacy names, and add the connection
- V3 (cleanup): take away the legacy fields as soon as the brand new knowledge is populated
Right here’s what the bridge schema may appear to be. Discover how the legacy values are saved round with @Attribute(originalName:) so that they nonetheless learn from the identical saved columns:
enum WeightSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Model(2, 0, 0)
static var fashions: [any PersistentModel.Type] = [WeightData.self, PerformedSet.self]
@Mannequin
remaining class WeightData {
@Attribute(originalName: "weight")
var legacyWeight: Float
@Attribute(originalName: "reps")
var legacyReps: Int
@Attribute(originalName: "units")
var legacySets: Int
@Relationship(inverse: WeightSchemaV2.PerformedSet.weightData)
var performedSets: [PerformedSet] = []
init(legacyWeight: Float, legacyReps: Int, legacySets: Int) {
self.legacyWeight = legacyWeight
self.legacyReps = legacyReps
self.legacySets = legacySets
}
}
@Mannequin
remaining class PerformedSet {
var weight: Float
var reps: Int
var units: Int
var weightData: WeightData?
init(weight: Float, reps: Int, units: Int, weightData: WeightData? = nil) {
self.weight = weight
self.reps = reps
self.units = units
self.weightData = weightData
}
}
}
Now you’ll be able to migrate by fetching WeightSchemaV2.WeightData in didMigrate and inserting a PerformedSet for every migrated WeightData:
static let migrateV1toV2 = MigrationStage.customized(
fromVersion: WeightSchemaV1.self,
toVersion: WeightSchemaV2.self,
willMigrate: nil,
didMigrate: { context in
let allWeightData = strive context.fetch(FetchDescriptor())
for weightData in allWeightData {
let performedSet = WeightSchemaV2.PerformedSet(
weight: weightData.legacyWeight,
reps: weightData.legacyReps,
units: weightData.legacySets,
weightData: weightData
)
weightData.performedSets.append(performedSet)
}
strive context.save()
}
)
When you’ve shipped this and also you’re assured the information is within the new form, you’ll be able to introduce V3 to take away legacyWeight, legacyReps, and legacySets fully. As a result of the information now lives in PerformedSet, V2 → V3 is usually a light-weight migration.
When you end up having to carry out a migration like this, it may be fairly scary and sophisticated so I extremely suggest correctly testing your app earlier than transport. Attempt testing migrations from and to totally different mannequin variations to just be sure you do not lose any knowledge.
Abstract
SwiftData migrations grow to be quite a bit much less tense while you deal with schema variations as a launch artifact. Introduce a brand new VersionedSchema while you ship mannequin modifications to customers, not for each little iteration you do throughout growth. That retains your migration story lifelike, testable, and manageable over time.
If you do ship a change, begin by asking whether or not SwiftData can moderately infer what to do. Light-weight migrations work nicely when no new necessities are launched: including non-obligatory fields, dropping fields, or renaming fields (so long as you map the unique saved identify). The second your change requires SwiftData to invent or derive a price—like introducing a brand new non-optional property, altering sorts, or composing values—you’re in handbook migration land, and a SchemaMigrationPlan with customized levels is the correct device.
For the actually difficult instances, don’t pressure all the pieces into one heroic migration. Add a bridge model, populate the brand new knowledge form first, then clear up outdated fields in a follow-up model. And no matter you do, take a look at migrations the way in which customers expertise them: migrate a retailer created by an older construct with messy knowledge, not a pristine simulator database you’ll be able to delete at will.

