I’ve developed an app some time ago using SwiftData
as a backing storage. Recently I needed to update the model, but not merely add or rename a property – these would require a lightweight migration and are done automatically. Boring. No, I needed to take one property and turn it into another one. In my case it was a conversion from date: Date?
to dateInt: Int?
. Don’t ask me why, it’s a long story but the problem here is that SwiftData
wouldn’t know how to automatically convert one to another so I have to show it and do the real complex schema migration. Or do I?
The posts you can find on the internet will show you some very simple use cases, like making a property unique. This still requires a complex (read “manual”) migration where you load all the models and then just filter them. But that ain’t solving my problem — I need to create a whole new model. Documentation for this particular part of SwiftData
is quite scarce and no AI agent could help me either so I had to roll up my sleeves and understand what’s going on there, and how these migrations are meant to be done. And now I’m gladly sharing my findings – how should you do the complex migrations and why you might not need one.
First let’s start with what I had originally:
|
|
Fairly simple, not much can be simplified. Now, apparently, a lot can be made more complicated. First, we need to create a migration plan for SwiftData
to know how to migrate from the previous model to the new one. But before we do that we should define both models (or rather the new one, we already have the old one). The clean approach to do this is to create something called VersionedSchema
. We will use both (v1 and v2) schemas inside the. migration plan but they also serve well as namespaces for the models. It would look something like this:
|
|
Notice how I moved the existing model to SchemaV1
, now it will live there. Now because you have your Foo
model namespaced the whole app will complain about not finding it anymore. You could replace all Foo
s with SchemaV2.Foo
s and get away with it (note, not V1 cause your app now would want to work with the newer model, right?) but the more elegant approach would be to declare a typealias
:
|
|
… and update it to SchemaV3
, …V4
etc when the time comes
Ok, now off to migration plan. The point is to show SwiftData
how to, well, migrate from one model to another. This is done using a MigrationStage
. You declare a migration stage and it will require you to provide the versions – schemas – and 2 optional closures: willMigrate
and didMigrate
. Sounds simple, we have access to 2 events, one fires before and one fires after the migration:
|
|
Now, as I mentioned, SwiftData
wouldn’t know how to migrate one to another so we’re here to help it. In some tutorials you’ll find that you can migrate everything in willMigrate
so let’s do it now:
|
|
Seems very easy, innit? Well, sorry to break it to you, this will crash. The reason? Cannot find SchemaV2.Foo
model.
WHAT?
Ok, I’m done with irony, here’s the undocumented thing (if there is any documentation, please point me to it): willMigrate
’s context
is only able to fetch models from the fromVersion
and the didMigrate
is the opposite – it only has access to toVersion
. In our case it’s SchemaV1
and SchemaV2
respectively. So I need to fetch the old models in willMigrate
and create new models and save them in didMigrate
. Let’s do this! Wait! How the heck are we gonna transfer the models between the closures? Oh boy, we have to store the old models somewhere and then retrieve it in didMigrate
. And this storage, of course, shouldn’t be SwiftData
… Because why would it, right? I promised to stop with irony, I’m sorry.
Here’s what I did:
|
|
Ok, so now we’re talking, this will work like a charm. Let me know if I did something stupid with all this Modern Swift Concurrency but it seems like the closures are running synchronous so we should be safe with nonisolated(unsafe)
here. But basically this is it, this is how you migrate the model with very different sets of parameters.
I could stop here if I wouldn’t opt in for iCloud sync though. The thing is, iCloud sync seems to only support lightweight migration and the one we do is not too light for it. You cannot do these types of migration cause imagine there are 2 clients that are in sync and then 1 is updated and migrated. How should the data be handled on the other device and in the cloud? Tough question. So yeah, I had to delete all the migration code I wrote and do something dirty but very simple and straightforward. I just left the date: Date?
in place and added a new dateInt: Int?
property to the original model. Then, upon opening the app I check if this “migration” has been already done and if not, then I just loop over all the foos
and populate dateInt
, setting date
to nil
. Now I have to make peace with the fact that I’m stuck with a property I’ll not use anymore but well, next time I might opt in for iCloud sync as late as possible.
Thank you very much for reading! As always, please contact me if you have any questions or suggestions: Contact