DEV Community

Utku Y.
Utku Y.

Posted on

SwiftData: MigrationStage.custom

Hi,

I just wasted the whole day on MigrationStage's custom option.

My first advice is: Just care which document are you reading.. It can lead you in the wrong direction if document is missing, not totally correct, etc.

My app has one model and I call it with Story.
and it look likes this:

enum StorySchema_Vx_x_x: VersionedSchema {
    static var versionIdentifier: Schema.Version = Schema.Version(x, x, x)

    static var models: [any PersistentModel.Type] {
        return [Story.self]
    }

    @Model
    class Story {
        var title: String
        var story: String
    ...
    }
...
}
Enter fullscreen mode Exit fullscreen mode

Now I have a new feature on app. However, due to the new design requirements I need to store Story.story as below.

struct StoryPage: Codable {
    let page: Int
    let story: String
}
Enter fullscreen mode Exit fullscreen mode

Here's the problem:

How should I store story on last database design?

  1. Replace var story: String with var storiesAsPart: [StoryPage]
  2. Just add new parameter as var storiesAsPart: [StoryPage]?

First approach

Of course my first decision is first one! Because I think it's cleaner as code, easier to understand. There are no any other distracting variables. Bla, bla, bla...

Ta-daa, That's why I wasted my whole day.

I couldn't find proper way to achieve first option.
Every development I tried, came with another issue...

Second approach

I stopped to waste my time. Because this feature need to be release ASAP due to market-trending.

But still got errors. WHY!?

Because wrong usage of parameters...

Solution

Do you remember what I wrote earlier?
You have to check if it is correct; what you read, what you watch, what you listen...

I just found a video(1) and realize how should I use MigrationStage.custom.

Let's see how achieve to this migration.

enum StoryMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        return [
            StorySchema_Vprevious_one.self,
            StorySchema_Vlatest_one.self
        ]
    }

    static var stages: [MigrationStage] {
        [
            migrate_Vprevious_one_TO_Vlatest_one
        ]
    }

    static let migrate_Vprevious_one_TO_Vlatest_one = MigrationStage.custom(
        fromVersion: StorySchema_Vprevious_one.self,
        toVersion: StorySchema_Vlatest_one.self,
        willMigrate: nil,
        didMigrate: { context in
            var stories: [StorySchema_Vlatest_one.Story]

            do {
                stories = try context.fetch(FetchDescriptor<StorySchema_Vlatest_one.Story>())
            } catch {
                fatalError("Failed to fetch stories: \(error)")
            }

            stories.forEach { story in
                story.storiesAsPart = [
                    StoryPage(page: 1, story: story.story)
                ]
            }

            do {
                try context.save()
            } catch {
                fatalError("Failed to save migration: \(error)")
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

We have two additional parameters compared to MigrationStage.lightweight: the willMigrate and didMigrate closures.

I was already familiar with these closures, having read a lot about them, but as I mentioned before, it's important to know if the documentation is correct.

On that video (1), I got that the meaning of those closures.
willMigrate: Is commonly used for clean up data, prepare data to migrate etc.
didMigrate: Update new model variables as you wish.

In my case, I used didMigrate because I just wanted to fill new variable (because I know that's enough and it's okay).

Last words

I didn't waste anymore time on this feature. So, that's why I chose the quicker solution from my perspective.

I still believe that it's possible to achieve the first approach.

Top comments (1)

Collapse
 
uy profile image
Utku Y.