> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Progressive Core Data Migrations
- URL: https://williamboles.com/progressive-core-data-migration/
- Published: 2019-02-04T13:48:20.000Z
- Updated: 2026-09-26T16:34:24.000Z
- Description: There are very few certainties in app development, but one is that once your app is released, it will change in unexpected ways. Meeting those changes can be painful. Here, we look at how to ease the pain of Core Data migrations.
- Author: William Boles
- Tags: Core Data, Migration

People really care about their possessions. Nowhere do you see this more than on public transport. It's not unusual to see bags occupying seats while people stand. As a Brit, we have developed a powerful form of communication to indicate that you want someone to move their bag - maybe a slight shuffle, fleeting eye contact with other standing commuters and low tutting. Even with these *clear* signs, some people have the audacity to ignore them and force you into doing the unthinkable - speaking to a stranger on public transport.

"Excuse me, could you please move your bag so that I can sit down?"

Surprisingly often, you are met with disdain as the other person consigns their bag to the indignity of the floor. As you settle into your seat, you begin thinking about how connected we are to our possessions.

People are not just deeply connected to physical items; we also feel the same way about our data. Especially if the data has been earned somehow - think about the sense of betrayal you feel when a game crashes and takes your newly unlocked *[thingamabob](https://www.thefreedictionary.com/thingamabob?ref=williamboles.com)* with it.

In our iOS apps, we often store these *thingamabobs* in [Core Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/index.html?ref=williamboles.com). The structure of which is defined by a model/schema - a set of entities with attributes and relationships. A common situation in development is that, over time, our model changes. When that model changes, we need to move our users' data from the old structure to the new one.

![Photo of a beautiful car](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/11/beautiful-car-min-1.jpg)

This post will explore how to perform a progressive Core Data migration. We will manipulate the built-in Core Data migration mechanism to avoid having to perform each migration as a single step and instead perform each migration as a series of steps.

> This post will gradually build up to a working example; however, if you're on a tight deadline and/or there is a murderous look creeping into your manager's eyes 😡, then head on over to the [completed example](https://github.com/wibosco/CoreDataMigrationRevised-Example?ref=williamboles.com) to see how things end up.

## The Migration Process

Core Data supports evolving the model over time by allowing us to create new versions of the model. Each version represents a snapshot of the schema at a point in time. A model version is only safely changeable between its creation and the moment it ships as part of an app update. Once released, a version is effectively "frozen" - any further changes to that version would result in an app crash upon launch. To change an already released model, you need to create a new version of that model and migrate users from the old version to the new version. Thankfully, Core Data has a built-in migration system.

> Before you go any further and start writing migration code, you need to first answer this question:
> 
> **"Do I need to bother with migration?"**
> 
> Seriously, just because you use Core Data in your project does not mean that you need to care about migrating the data stored there. If you only use Core Data as a local cache and always override it with the content you get from an API response, you probably don't need to go through the effort of migrating from one version to another. Just delete the local `.sqlite` files and recreate your Core Data stack, populating that new model with calls to the API.
> 
> If that applies to you, you can stop reading now if you want or continue on with a certain smugness knowing that the difficulties being described below do not relate to you 😜.

Migrations can be handled using one of two techniques:

1. [Lightweight Migration](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmLightweightMigration.html?ref=williamboles.com) \- when Core Data can automatically infer how the migration should happen and create the mapping model on the fly.
2. [Standard Migration](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmMappingOverview.html?ref=williamboles.com###//apple%5Fref/doc/uid/TP40004399-CH5-SW1) \- when Core Data cannot infer how the migration should happen, we must write a custom migration by providing a mapping model (`xcmappingmodel`) and/or a migration policy ([NSEntityMigrationPolicy](https://developer.apple.com/documentation/coredata/nsentitymigrationpolicy?ref=williamboles.com)).

> It's worth noting that both techniques produce a mapping model. The difference is where it comes from - Standard migrations use one that lives in the project as an `xcmappingmodel` file, while Lightweight migrations have Core Data generate one on the fly.

By default, Core Data will attempt to perform a migration automatically when it detects a mismatch between the model used in the persistent store and the bundle's **current** model. When this happens, Core Data will first attempt to perform a Standard migration by searching in the app's bundle for a mapping model that maps from the persistent store model to the **current** bundle model. If a custom mapping model isn't found, Core Data will then attempt to perform a Lightweight migration. If neither form of migration is possible, an exception is thrown.

> If you are using `NSPersistentContainer`, Lightweight migrations are enabled by default; however, if you are still directly setting up the `NSPersistentStoreCoordinator`, then you need to enable Lightweight migrations by passing in an `options` dictionary with both `NSMigratePersistentStoresAutomaticallyOption` and `NSInferMappingModelAutomaticallyOption` set to `true` when loading the persistent store.

These automatic migrations are performed as one-step migrations; directly from the source to the destination model. So if we support four model versions, mapping models would exist for `1 to 4`, `2 to 4` and `3 to 4`. While this is the most efficient migration approach from a device performance point-of-view, it can be quite wasteful from a development point-of-view. For example, if we added a new model version (`5`), we would need to create **four new mapping models** from `1 to 5`, `2 to 5`, `3 to 5` and `4 to 5`. As you can see, none of the mapping models for migrating to version `4` can be reused. With a one-step migration approach, each newly added model version requires `n-1` mapping models (where `n` is the number of supported model versions) to be created. All of that extra work may eventually convince us to prematurely drop support for migrating from certain versions 😞.

We can reduce this work by disabling automatic migrations and chaining multiple smaller migrations together. When adding a new model version, we only need to handle migrating from its direct predecessor - `4 to 5` \- because we can reuse the existing `1 to 2`, `2 to 3` and `3 to 4` mapping models.

Manual migrations also reduce the complexity of each migration. Version `4` is much nearer to the structure of version `5` than version `1` is, so the conceptual distance each mapping has to cover is smaller - which should make it easier to spot any issues with the migration.

## Progressive Migrations

To support progressive migrations, we'll need to answer a few questions:

1. Which model version comes after *version X*?
2. What is a migration step?
3. How can we combine the migration steps into a migration path?
4. How do we trigger a migration?

The following types will answer these questions:

![Class diagram showing the progressive Core Data migration architecture. CoreDataManager sets up and uses CoreDataMigrator. CoreDataMigrator, in turn, uses CoreDataMigrationVersion and creates CoreDataMigrationStep. CoreDataError and CoreDataMigrationError sit alongside as the error types thrown across the migration flow](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2026/04/progressive-core-data-migration-class-diagram-1.jpg)

- `CoreDataManager` sets up the Core Data stack and acts as the entry point for migrations. Before loading the persistent store, it checks whether a migration is needed and, if so, hands the work off to `CoreDataMigrator`.
- `CoreDataMigrator` orchestrates the migration process. It determines whether a migration is required by comparing the persistent store's metadata against the current model, forces [WAL checkpointing](https://sqlite.org/wal.html?ref=williamboles.com) to ensure data integrity, builds the full migration path from source to destination, and then iterates through each step - writing intermediate results to temporary stores before replacing the original.
- `CoreDataMigrationVersion` represents a Core Data model version as an enum case. It defines the migration graph by mapping each version to its successor, making it possible to see the entire migration path at a glance.
- `CoreDataMigrationStep` is a simple value type that holds the three pieces needed to perform a single migration: the source model, the destination model and the mapping model that bridges them.
- `CoreDataError` covers failures that can occur when setting up the Core Data stack.
- `CoreDataMigrationError` covers failures that can occur during the migration process.

> Don't worry if that doesn't all make sense yet; we will look into each type in greater depth below.

> You'll notice cases from `CoreDataError` and `CoreDataMigrationError` being thrown throughout the code below. To keep the focus on the migration logic, I won't show the full enum definitions in the post - if you want to see them, head over to the [completed example](https://github.com/wibosco/CoreDataMigrationRevised-Example?ref=williamboles.com).

### Which Model Version Comes After *Version X*?

Each `CoreDataMigrationVersion` instance will represent a Core Data model version. As each Core Data model version is unique and known at compile time, they can be perfectly represented as enum cases:

```
// 1
enum CoreDataMigrationVersion: String, CaseIterable {
    case version1 = "CoreDataMigration_Example"

    // 2
    static func current() throws -> CoreDataMigrationVersion {
        guard let current = allCases.last else {
            throw CoreDataMigrationError.modelNotFound
        }

        return current
    }
}
```

Here's what we did:

1. `CoreDataMigrationVersion` is a `String` backed enum where each case maps to a model version in the `*.xcdatamodeld` package - providing a type-safe representation of those versions. It conforms to [CaseIterable](https://developer.apple.com/documentation/swift/caseiterable?ref=williamboles.com) to allow its cases to be treated as an ordered array.
2. `current()` returns the latest model version by grabbing the last element from `allCases`. Because cases are returned in declaration order, adding a new version to the end of the enum is all that's needed to update this property.

The real value of `CoreDataMigrationVersion` comes in its ability to determine which model version should come after any given `CoreDataMigrationVersion` case:

```
enum CoreDataMigrationVersion: String, CaseIterable {
    // Omitted unchanged functionality

    // 1
    func nextVersion() -> CoreDataMigrationVersion? {
        switch self {
        case .version1:
            return nil
        }
    }
}
```

1. `nextVersion()` determines which version comes after `self` in the migration graph. As we only have one version, it returns `nil`. As new versions are added, each case will map to its successor, forming the step-by-step migration chain.

As we only have one model at the moment, the implementation of `nextVersion()` is simple, but eventually it could end up with something like:

```
enum CoreDataMigrationVersion: String, CaseIterable {
    // Omitted unchanged functionality

    func nextVersion() -> CoreDataMigrationVersion? {
        switch self {
        case .version1:
            return .version2
        case .version2:
            return .version3
        case .version3:
            return .version4
        case .version4:
            return nil
        }
    }
}
```

Writing a switch statement here may look like overkill - why not just always choose the next enum case? But that assumes the migration graph is always a straight line, and real projects rarely are. The switch earns its keep in two ways.

#### Existing Single-Step Migrations Can Be Folded In

A project that's already performed several single-step migrations will have mapping models and possibly migration policies in place. That work doesn't have to be thrown away - those existing paths can live inside `nextVersion()` as big jumps, with progressive migrations taking over from whatever version the project is on now.

> If you're reading this post before performing your first migration, congratulations on your:
> 
> 1. Excellent taste in selecting blog posts.
> 2. Organisational ability.

In a hypothetical project with **six** model versions, which used the one-step migration approach up to version `4` before switching over to the progressive migration approach, `nextVersion()` would look like:

```
func nextVersion() -> CoreDataMigrationVersion? {
    switch self {
    case .version1, .version2, .version3: // single-step migration
        return .version4
    case .version4: // progressive migration
        return .version5
    case .version5: // progressive migration
        return .version6
    case .version6:
        return nil
    }
}
```

In the above code snippet, `version1`, `version2` and `version3` migrate directly to `version4`, and then `version4` and `version5` migrate to their direct successor. The two migration approaches co-exist very happily with each other.

#### Broken Versions Can Be Routed Around

Shipping broken code happens. If a model version makes it into production and corrupts data on migration, `nextVersion()` can be configured to bypass that version entirely so that any currently unaffected users skip over it:

```
func nextVersion() -> CoreDataMigrationVersion? {
    switch self {
    case .version1:
        return .version2
    case .version2:
        return .version4 // skipping corrupted .version3
    case .version3:
        return .version4
    case .version4:
        return nil
    }
}
```

Both situations are handled inside `nextVersion()`, without leaking into the rest of the migration code.

Now that we know which version is next, we need to build the path between the two versions.

### What Is a Migration Step?

A migration happens between two model versions by mapping the entities, attributes and relationships of the source model to their counterparts in the destination model. As such, `CoreDataMigrationStep` needs to contain three properties:

```
struct CoreDataMigrationStep {
    // 1
    let sourceModel: NSManagedObjectModel

    // 2
    let destinationModel: NSManagedObjectModel

    // 3
    let mappingModel: NSMappingModel
}
```

1. The source model represents the schema this step migrates *from*.
2. The destination model represents the schema this step migrates *to*.
3. The mapping model describes *how* to transform data between the two. This mapping will either be Lightweight or Standard - both of which use a [NSMappingModel](https://developer.apple.com/documentation/coredata/nsmappingmodel?ref=williamboles.com) instance to hold the mapping path between the versions.

`CoreDataMigrationStep` won't be handed these values but will be responsible for setting its own properties:

```
struct CoreDataMigrationStep {
    // Omitted unchanged functionality

    // 1
    init(sourceVersion: CoreDataMigrationVersion,
         destinationVersion: CoreDataMigrationVersion) throws {
        let sourceModel = try NSManagedObjectModel.managedObjectModel(forResource: sourceVersion.rawValue)
        let destinationModel = try NSManagedObjectModel.managedObjectModel(forResource: destinationVersion.rawValue)

        guard let mappingModel = CoreDataMigrationStep.mappingModel(from: sourceModel,
                                                                    to: destinationModel) else {
            throw CoreDataMigrationError.mappingModelNotFound
        }

        self.sourceModel = sourceModel
        self.destinationModel = destinationModel
        self.mappingModel = mappingModel
    }

    // 2
    private static func mappingModel(from sourceModel: NSManagedObjectModel,
                                     to destinationModel: NSManagedObjectModel) -> NSMappingModel? {
        return customMappingModel(from: sourceModel, to: destinationModel) ?? inferredMappingModel(from: sourceModel, to: destinationModel)
    }

    // 3
    private static func customMappingModel(from sourceModel: NSManagedObjectModel,
                                           to destinationModel: NSManagedObjectModel) -> NSMappingModel? {
        return NSMappingModel(from: [Bundle.main],
                              forSourceModel: sourceModel,
                              destinationModel: destinationModel)
    }

    // 4
    private static func inferredMappingModel(from sourceModel: NSManagedObjectModel,
                                             to destinationModel: NSManagedObjectModel) -> NSMappingModel? {
        return try? NSMappingModel.inferredMappingModel(forSourceModel: sourceModel,
                                                        destinationModel: destinationModel)
    }
}
```

1. The initialiser takes a source and destination version, loads their corresponding `NSManagedObjectModel` instances from the bundle, then attempts to resolve a mapping model between them - throwing an error if no mapping exists.
2. `mappingModel(from:to:)` is the resolution strategy. If a custom mapping model exists, then that is favoured over an inferred mapping model.
3. `customMappingModel(from:to:)` searches the main bundle for a `.xcmappingmodel` that matches the two models to perform a Standard migration.
4. `inferredMappingModel(from:to:)` asks Core Data to automatically figure out the mapping based on model differences to perform a Lightweight migration. The `try?` swallows any error and returns `nil` instead - meaning if inference fails, we treat it as *no mapping available*.

> It's possible to have multiple mapping models between versions (this can be especially useful when migrating large data sets). In this post, in an attempt to keep things simple, I assume only one mapping model.

### How Can We Combine the Migration Steps into a Migration Path?

`CoreDataMigrator` is at the heart of our migration solution and has **three** tasks:

1. Determining if there needs to be a migration.
2. Ensuring the persistent store is ready to be migrated.
3. Performing the migration.

#### Determining if There Needs to Be a Migration

```
class CoreDataMigrator {

    // 1
    func requiresMigration(at storeURL: URL,
                           toVersion version: CoreDataMigrationVersion) throws -> Bool {

        // 2
        guard let metadata = try? NSPersistentStoreCoordinator.metadata(at: storeURL) else {
            return false
        }

        // 3
        let compatibleVersion = try CoreDataMigrationVersion.compatibleVersionForStoreMetadata(metadata)

        // 4
        return compatibleVersion != version
    }
}
```

1. `requiresMigration(at:toVersion:)` takes the URL of the persistent store and the model version we want to migrate to, and returns whether a migration is needed.
2. The persistent store's metadata is loaded from the store URL. If no metadata can be found - for example, because the store doesn't exist yet - then no migration is needed, so we return `false`.
3. The metadata is used to determine which model version the persistent store is currently compatible with.
4. If the store's version doesn't match the version we want to migrate to, a migration is needed.

To support this, we need to extend `CoreDataMigrationVersion` to include:

```
// 1
private extension CoreDataMigrationVersion {

    // 2
    static func compatibleVersionForStoreMetadata(_ metadata: [String : Any]) throws -> CoreDataMigrationVersion {

        // 3
        let compatibleVersion = CoreDataMigrationVersion.allCases.first {
            guard let model = try? NSManagedObjectModel.managedObjectModel(forResource: $0.rawValue) else {
                return false
            }

            // 4
            return model.isConfiguration(withName: nil,
                                         compatibleWithStoreMetadata: metadata)
        }

        // 5
        guard let compatibleVersion = compatibleVersion else {
            throw CoreDataMigrationError.versionUnknown
        }

        return compatibleVersion
    }
}
```

1. `compatibleVersionForStoreMetadata(_:)` is added as a `private extension` rather than being folded into the main `CoreDataMigrationVersion` declaration. This is migrator-specific scaffolding - it reaches into the bundle to load models, which isn't a concern of the version enum itself. Keeping it in a `private` extension signals that it's an implementation detail of the migrator and prevents the rest of the app from accidentally depending on it.
2. `compatibleVersionForStoreMetadata(_:)` takes the persistent store's metadata and returns the `CoreDataMigrationVersion` case that matches it.
3. Each case in `CoreDataMigrationVersion` is iterated through, and for each case, the associated managed object model is loaded from the bundle using the case's raw value.
4. The loaded model is checked against the store's metadata to see if they are compatible - that is, whether the store was created with (or last migrated to) that model version.
5. If no compatible version is found, an error is thrown as the store is in an unknown state that we can't migrate from.

#### Ensuring the Persistent Store Is Ready to Be Migrated

Before attempting a migration, we need to undertake some housekeeping on our persistent store.

Since iOS 7, Core Data has used the [Write-Ahead Logging (WAL)](https://www.sqlite.org/wal.html?ref=williamboles.com) option on SQLite stores to provide the ability to recover from crashes by allowing changes to be rolled back until the database is stable. If you have ever had to perform a rollback before, the WAL approach may work a little differently from what you are expecting. Rather than directly writing changes to the `sqlite` file and having a pre-write copy of the changes to rollback to, in WAL mode, the changes are first written to the `sqlite-wal` file, and at some future date, those changes are transferred to the `sqlite` file. The `sqlite-wal` file is in effect an up-to-date copy of some of the data stored in the main `sqlite` file.

The `sqlite-wal` and `sqlite` files store their data using the same structure to allow data to be transferred easily between them. However, this shared structure causes issues during migration as Core Data only migrates the data stored in the `sqlite` file to the new structure, leaving the data in the `sqlite-wal` file in the old structure. The resulting mismatch in structure will lead to a crash when Core Data attempts to update/use data stored in the `sqlite-wal` file 😞. To avoid this crash, we need to force any data in the `sqlite-wal` file into the `sqlite` file before we perform a migration - a process known as `checkpointing`:

```
class CoreDataMigrator {
    // Omitted unchanged functionality

    // 1
    func forceWALCheckpointingForStore(at storeURL: URL) throws {

        // 2
        let metadata = try NSPersistentStoreCoordinator.metadata(at: storeURL)
        guard let currentModel = NSManagedObjectModel.compatibleModelForStoreMetadata(metadata) else {
            throw CoreDataMigrationError.walCheckpointingMissingCompatibleModel
        }

        // 3
        do {
            let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: currentModel)

            let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
            let store = try persistentStoreCoordinator.addPersistentStore(at: storeURL,
                                                                          options: options)
            try persistentStoreCoordinator.remove(store)
        } catch let error {
            throw CoreDataMigrationError.walCheckpointingFailed(error)
        }
    }
}
```

1. `forceWALCheckpointingForStore(at:)` forces any data sitting in the `sqlite-wal` file into the main `sqlite` file before a migration is attempted.
2. The store's metadata is loaded and used to find the compatible managed object model. It's important that the store's model is used here rather than the bundle's current model - we need to checkpoint against the model the store was actually created with.
3. A temporary persistent store coordinator is created, and the store is added with the `journal_mode` set to `DELETE`. This forces SQLite to flush any pending WAL data into the main `sqlite` file and then delete the now-empty `sqlite-wal` file. Once checkpointing is complete, the store is removed from the coordinator.

> An easy mistake when `checkpointing` is to use the bundle's model rather than the store's model - remember we want to perform `checkpointing` on the live (store) model before attempting to migrate to the latest (bundle) model.

#### Performing the Migration

Before starting the migration process, we first need to chain the individual migration steps into a progressive migration path:

```
class CoreDataMigrator {
    // Omitted unchanged functionality

    // 1
    private func migrationStepsForStore(at storeURL: URL,
                                        toVersion destinationVersion: CoreDataMigrationVersion)  throws -> [CoreDataMigrationStep] {

        // 2
        let metadata = try NSPersistentStoreCoordinator.metadata(at: storeURL)
        let sourceVersion = try CoreDataMigrationVersion.compatibleVersionForStoreMetadata(metadata)

        return try migrationSteps(fromSourceVersion: sourceVersion,
                                  toDestinationVersion: destinationVersion)
    }

    // 3
    private func migrationSteps(fromSourceVersion sourceVersion: CoreDataMigrationVersion,
                                toDestinationVersion destinationVersion: CoreDataMigrationVersion) throws -> [CoreDataMigrationStep] {
        var sourceVersion = sourceVersion
        var migrationSteps = [CoreDataMigrationStep]()

        // 4
        while sourceVersion != destinationVersion, let nextVersion = sourceVersion.nextVersion() {
            let migrationStep = try CoreDataMigrationStep(sourceVersion: sourceVersion,
                                                          destinationVersion: nextVersion)
            migrationSteps.append(migrationStep)

            sourceVersion = nextVersion
        }

        return migrationSteps
    }
}
```

1. `migrationStepsForStore(at:toVersion:)` builds the full list of migration steps needed to bring the persistent store from its current model version to the destination model version.
2. The store's metadata is loaded and used to determine which model version the store is currently on. That source version and the destination version are then passed into `migrationSteps(fromSourceVersion:toDestinationVersion:)`.
3. `migrationSteps(fromSourceVersion:toDestinationVersion:)` constructs the ordered array of `CoreDataMigrationStep` instances that form the migration path.
4. Starting from the source version, each version is asked for its `nextVersion()`. A `CoreDataMigrationStep` is created for each pair of consecutive versions and appended to the array. This continues until the destination version is reached, producing the complete chain of steps needed for the progressive migration.

With the path built, it's time to kick off the migration process:

```
class CoreDataMigrator {
    // Omitted unchanged functionality

    // 1
    func migrateStore(at storeURL: URL,
                      toVersion version: CoreDataMigrationVersion) throws {
        // 2
        try forceWALCheckpointingForStore(at: storeURL)

        // 3
        var currentURL = storeURL
        let migrationSteps = try self.migrationStepsForStore(at: storeURL,
                                                             toVersion: version)

        // 4
        for migrationStep in migrationSteps {
            // 5
            let manager = NSMigrationManager(sourceModel: migrationStep.sourceModel,
                                             destinationModel: migrationStep.destinationModel)
            let tmpDirectory = URL(fileURLWithPath: NSTemporaryDirectory(),
                                   isDirectory: true)
            let destinationURL = tmpDirectory.appendingPathComponent(UUID().uuidString)

            // 6
            do {
                try manager.migrateStore(from: currentURL,
                                         sourceType: NSSQLiteStoreType,
                                         options: nil,
                                         with: migrationStep.mappingModel,
                                         toDestinationURL: destinationURL,
                                         destinationType: NSSQLiteStoreType,
                                         destinationOptions: nil)
            } catch let error {
                throw CoreDataMigrationError.migrationFailed(error)
            }

            // 7
            if currentURL != storeURL {
                //Destroy intermediate step's store
                try NSPersistentStoreCoordinator.destroyStore(at: currentURL)
            }

            currentURL = destinationURL
        }

        // 8
        try NSPersistentStoreCoordinator.replaceStore(at: storeURL,
                                                      withStoreAt: currentURL)

        // 9
        if currentURL != storeURL {
            try NSPersistentStoreCoordinator.destroyStore(at: currentURL)
        }
    }
}
```

1. `migrateStore(at:toVersion:)` performs the full progressive migration, taking the persistent store from its current model version to the destination version.
2. Before anything else, WAL checkpointing is forced so that all data is flushed from the `sqlite-wal` file into the main `sqlite` file.
3. The migration path is built as an ordered array of `CoreDataMigrationStep` instances, and `currentURL` is set to track which store file should be used as the source for the next step.
4. Each migration step is iterated through in order.
5. For each step, an [NSMigrationManager](https://developer.apple.com/documentation/coredata/nsmigrationmanager?ref=williamboles.com) is created with the step's source and destination models, and a temporary file URL is generated to hold the output of this step.
6. The migration manager migrates the data from `currentURL` to the temporary destination URL using the step's mapping model. If the migration fails, the error is wrapped and thrown.
7. If the source for this step was itself a temporary file from a previous step, it is destroyed - we no longer need it. `currentURL` is then updated to point at the newly created temporary store, ready for the next iteration.
8. Once all steps have been completed, the original persistent store is replaced with the fully migrated temporary store.
9. The final temporary store is cleaned up, leaving only the original store URL with the fully migrated data.

It's important to emphasise that we only overwrite the store once we know that all migration steps have been successfully completed. If any individual step fails, an error is thrown, and the original store is left untouched - we never end up in a corrupt state. Overwriting as the final step also speeds up the development of a custom migration path - as the source store is never mutated until the very end, we can iterate on a broken migration policy without having to reset our test data between attempts.

In the above code snippets, we've seen a number of methods used that are not part of the standard API, so I've included the extensions that contain these methods below. As with most extensions, the methods are used to reduce boilerplate code.

Let's start with those extensions on `NSPersistentStoreCoordinator`:

```
extension NSPersistentStoreCoordinator {

    // 1
    static func destroyStore(at storeURL: URL) throws {
        do {
            let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: NSManagedObjectModel())
            try persistentStoreCoordinator.destroyPersistentStore(at: storeURL,
                                                                  ofType: NSSQLiteStoreType,
                                                                  options: nil)
        } catch let error {
            throw CoreDataMigrationError.storeDestructionFailed(error)
        }
    }

    // 2
    static func replaceStore(at targetURL: URL,
                             withStoreAt sourceURL: URL) throws {
        do {
            let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: NSManagedObjectModel())
            try persistentStoreCoordinator.replacePersistentStore(at: targetURL,
                                                                  destinationOptions: nil,
                                                                  withPersistentStoreFrom: sourceURL,
                                                                  sourceOptions: nil,
                                                                  ofType: NSSQLiteStoreType)
        } catch let error {
            throw CoreDataMigrationError.storeReplacementFailed(error)
        }
    }

    // 3
    static func metadata(at storeURL: URL) throws -> [String: Any]  {
        do {
            return try NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType,
                                                                                at: storeURL,
                                                                                options: nil)
        } catch let error {
            throw CoreDataMigrationError.metadataUnknown(error)
        }
    }

    // 4
    func addPersistentStore(at storeURL: URL,
                            options: [AnyHashable: Any]) throws -> NSPersistentStore {
        do {
            return try addPersistentStore(ofType: NSSQLiteStoreType,
                                          configurationName: nil,
                                          at: storeURL,
                                          options: options)
        } catch let error {
            throw CoreDataMigrationError.storeAdditionFailed(error)
        }
    }
}
```

1. `destroyStore(at:)` deletes the persistent store at the given URL. A temporary coordinator is created with an empty model purely to access the `destroyPersistentStore(at:ofType:options:)` method.
2. `replaceStore(at:withStoreAt:)` overwrites the persistent store at the target URL with the store at the source URL. As with `destroyStore(at:)`, a temporary coordinator is created to access the replacement API.
3. `metadata(at:)` loads and returns the metadata dictionary for the persistent store at the given URL.
4. `addPersistentStore(at:options:)` is an instance method that wraps the verbose `addPersistentStore(ofType:configurationName:at:options:)` call with sensible defaults for an SQLite store.

Let's move on to the `NSManagedObjectModel` extension:

```
extension NSManagedObjectModel {

    // 1
    static func compatibleModelForStoreMetadata(_ metadata: [String : Any]) -> NSManagedObjectModel? {
        let mainBundle = Bundle.main
        return NSManagedObjectModel.mergedModel(from: [mainBundle],
                                                forStoreMetadata: metadata)
    }

    // 2
    static func managedObjectModel(forResource resource: String) throws -> NSManagedObjectModel {
        let mainBundle = Bundle.main
        let subdirectory = "CoreDataMigration_Example.momd"

        let omoURL = mainBundle.url(forResource: resource,
                                    withExtension: "omo",
                                    subdirectory: subdirectory) // optimised model file

        let momURL = mainBundle.url(forResource: resource,
                                    withExtension: "mom",
                                    subdirectory: subdirectory)

        guard let url = omoURL ?? momURL,
              let model = NSManagedObjectModel(contentsOf: url) else {
            throw CoreDataMigrationError.unableToLoadModel
        }

        return model
    }
}
```

1. `compatibleModelForStoreMetadata(_:)` returns the managed object model from the main bundle that is compatible with the given store metadata, or `nil` if no match is found.
2. `managedObjectModel(forResource:)` loads a specific model version from the bundle by name. It first looks for an `omo` file (the optimised model format), falling back to the standard `mom` file if the optimised version isn't available.

Now that migrations are possible, we need to set up our Core Data stack.

### How Do We Trigger a Migration?

`CoreDataManager` handles both setting up the Core Data stack and triggering a migration (if needed). Let's see our stack:

```
class CoreDataManager {

    // 1
    lazy var persistentContainer: NSPersistentContainer = {
        let persistentContainer = NSPersistentContainer(name: "CoreDataMigration_Example")
        let description = persistentContainer.persistentStoreDescriptions.first
        description?.shouldInferMappingModelAutomatically = false
        description?.shouldMigrateStoreAutomatically = false

        return persistentContainer
    }()

    // 2
    lazy var backgroundContext: NSManagedObjectContext = {
        let context = self.persistentContainer.newBackgroundContext()
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        return context
    }()

    // 3
    lazy var mainContext: NSManagedObjectContext = {
        let context = self.persistentContainer.viewContext
        context.automaticallyMergesChangesFromParent = true

        return context
    }()

    // MARK: - Singleton

    // 4
    static let shared = CoreDataManager()

    // 5
    func setup(completion: @escaping (Result<Void, Error>) -> Void) {
        loadPersistentStore { result in
            completion(result)
        }
    }

    // 6
    private func loadPersistentStore(completion: @escaping (Result<Void, Error>) -> Void) {
        self.persistentContainer.loadPersistentStores { _, error in
            guard let error = error else {
                completion(.success(()))
                return
            }

            completion(.failure(CoreDataError.persistentStoreLoadFailed(error)))
        }
    }
}
```

1. The [NSPersistentContainer](https://developer.apple.com/documentation/coredata/nspersistentcontainer?ref=williamboles.com) is created with both `shouldInferMappingModelAutomatically` and `shouldMigrateStoreAutomatically` set to `false`. This disables Core Data's built-in automatic migrations so that our progressive migration approach can handle them instead.
2. A background context is created for performing Core Data operations off the main thread, with a merge policy that favours in-memory changes over store changes in the event of a conflict.
3. The main context is the container's `viewContext`, configured to automatically merge changes saved by other contexts - such as the background context above.
4. `CoreDataManager` is a singleton, providing a single shared Core Data stack for the app.
5. `setup(completion:)` is the entry point for initialising the Core Data stack, delegating to `loadPersistentStore(completion:)`.
6. `loadPersistentStore(completion:)` loads the persistent store and returns the result through a completion closure. If loading fails, the error is wrapped in a `CoreDataError` case.

With our Core Data stack setup, let's add support for migrating:

```
class CoreDataManager {
    // Omitted unchanged functionality

    private func loadPersistentStore(completion: @escaping (Result<Void, Error>) -> Void) {
        // 1
        migrateStoreIfNeeded { result in
            guard case .success = result else {
                completion(result)
                return
            }

            self.persistentContainer.loadPersistentStores { _, error in
                guard let error = error else {
                    completion(.success(()))
                    return
                }

                completion(.failure(CoreDataError.persistentStoreLoadFailed(error)))
            }
        }
    }

    // 2
    private func migrateStoreIfNeeded(completion: @escaping (Result<Void, Error>) -> Void) {
        guard let storeURL = persistentContainer.persistentStoreDescriptions.first?.url else {
            completion(.failure(CoreDataError.persistentContainerNotSetup))
            return
        }

        // 3
        do {
            let currentVersion = try CoreDataMigrationVersion.current()
            let requiresMigration = try migrator.requiresMigration(at: storeURL,
                                                                   toVersion: currentVersion)

            guard requiresMigration else {
                completion(.success(()))
                return
            }

            migrateStore(storeURL: storeURL,
                         toVersion: currentVersion,
                         completion: completion)
        } catch {
            completion(.failure(error))
        }
    }

    // 4
    private func migrateStore(storeURL: URL,
                              toVersion version: CoreDataMigrationVersion,
                              completion: @escaping (Result<Void, Error>) -> Void) {

        // 5
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                try self.migrator.migrateStore(at: storeURL,
                                               toVersion: version)
            } catch {
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
                return
            }

            // 6
            DispatchQueue.main.async {
                completion(.success(()))
            }
        }
    }
}
```

1. `loadPersistentStore(completion:)` now checks if a migration is needed before loading the persistent store. The store is only loaded once the migration has completed successfully.
2. `migrateStoreIfNeeded(completion:)` retrieves the store URL from the persistent container and determines whether a migration is required.
3. The current model version is resolved and compared against the store's version. If no migration is needed, the method returns early with success. Otherwise, the migration is kicked off.
4. `migrateStore(storeURL:toVersion:completion:)` performs the actual migration using the migrator.
5. The migration is dispatched onto a background queue to avoid blocking the main thread - migrations can be slow, particularly for large data sets.
6. Once the migration completes, the result is dispatched back to the main queue so that the caller can safely update the UI.

`CoreDataManager` is called in the `SceneDelegate`:

```
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        // 1
        CoreDataManager.shared.setup { result in
            // 2
            guard case let .failure(error) = result else {
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { // just for example purposes
                    self.presentMainUI()
                }

                return
            }

            // 3
            fatalError("Unable to set up Core Data stack: \(error)")
        }
    }
}
```

1. The Core Data stack is set up when the scene connects, before the user is allowed into the app.
2. If setup succeeds, the main UI is presented - replacing the loading screen with a navigation stack that can freely use Core Data.
3. If setup fails, a fatal error is thrown. In a production app, you would want to handle this more gracefully.

> The above code snippet is from the example project where the user is shown a loading screen while the Core Data stack is being set up. Only once the setup is complete is the user allowed into the *app proper*. `presentMainUI()` switches out the window's root view controller for a navigation stack that can freely use Core Data. While this is strictly not necessary, by splitting the UI into `pre` and `post` *Core Data stack set-up*, it is possible to avoid race conditions where the app is attempting to use Core Data before it has finished setting up.

## 💃🥂🎉🕺

**Congratulations, that's all there is to the progressive migration approach.**

The rest of this post is devoted to putting the above migration approach into practice by migrating an app through **three** Core Data model versions.

## Colourful Posts

`Colourful Posts` is a simple app that allows the user to create posts that are persisted in Core Data. Each post consists of:

- A unique ID.
- A random associated colour represented as a hex string.
- The body/content of the post.
- The date the post was created.

So that the model looks like:

![Screenshot showing Colourful Posts Core Data model](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/01/Screenshot-showing-colourful-posts-model-1.png)

Each post that the user creates is then displayed in a tableview as a brightly coloured cell.

![Screenshot of Colourful Posts UI](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/2019/01/screenshot-of-colourful-posts-2.png)

> To keep this post to a reasonable length, I won't show any code from `Colourful Posts` that isn't connected to performing a migration.

It's a simple, fun app that we submit to Apple for approval 🤞.

#### Migrating to Version 2

Despite not being able to edit posts, Apple not only approves `Colourful Posts`, they love it so much so that they feature it on the `Today` tab. `Colourful Posts` is instantly propelled to the top of the charts. After hundreds of thousands of downloads, we decide to hire a new developer to help steer the success-train we find ourselves on 🚂. However, in their first week, the new developer mistakes the information stored in the `color` property on `Post` as using RGB rather than hex to store the colour as a string. Unfortunately, we don't catch this mismatch until it's in production and leads to the app crashing on launch 😞. To avoid this issue from happening when we hire more developers, we decide to rename `color` to `hexColor`. As this is a change to the model, we need to create a new model version and handle the migration between the old and new versions.

> To create a new model version, select the `*.xcdatamodel` (it may be called `*.xcdatamodeld`) file in the `Project Navigator`, open the `Editor` menu from the top bar and click on the `Add Model Version...` option. In the wizard that opens, this new model will already be given a name, which typically follows `[ModelName] [Number]`, e.g. `CoreDataMigration_Example 2`, but the name can be changed to whatever you want.

Lightweight migrations are typically a less intensive form of migration than Standard migrations (from both a developer and [performance point-of-view](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmLightweightMigration.html?ref=williamboles.com)). Because of this, I prefer to perform Lightweight migrations whenever possible. Lightweight migrations can handle the following transformations to the model:

- Adding an attribute.
- Removing an attribute.
- Changing a non-optional attribute to be optional.
- Changing an optional attribute to non-optional (by defining a default value).
- Renaming an entity, attribute or relationship (by providing a `Renaming ID`).
- Adding a relationship.
- Removing a relationship.
- Changing the entity hierarchy.

An impressive list of transformations that we get *free* (or almost free) with Lightweight migrations. The `color` to `hexColor` change is covered by the `Renaming an entity, attribute or relationship`, which has a small caveat: `by providing a Renaming ID`. The `Renaming ID` creates a link between the old attribute and the new attribute. All it requires is to add the old attribute name to the new attribute's metadata:

![Screenshot showing Renaming ID with a value of: color](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/01/Screenshot-showing-renaming-id-3.png)

With this information, Core Data now knows that `color` and `hexColor` are the same attribute, just with different names. Rather than discarding `color` during a Lightweight migration, the value is transferred to `hexColor`.

With that change, the only thing that's left to do is update `CoreDataMigrationVersion` to allow migrations from `CoreDataMigration_Example` to `CoreDataMigration_Example 2`:

```
enum CoreDataMigrationVersion: String, CaseIterable {
    case version1 = "CoreDataMigration_Example"
    case version2 = "CoreDataMigration_Example 2"

    // Omitting unchanged functionality

    func nextVersion() -> CoreDataMigrationVersion? {
        switch self {
        case .version1:
            return .version2
        case .version2:
            return nil
        }
    }
}
```

A new case was added to `CoreDataMigrationVersion` \- `version2`. As with `version1`, this new version has a raw value which maps to the name of its respective model version - `CoreDataMigration_Example 2`. `nextVersion()` has also been updated so that there is a migration path from `version1` to `version2`.

Now that we have a migration path, let's look at unit testing it. Unit testing a migration path requires:

1. Populating a SQLite database using the `CoreDataMigration_Example` model.
2. Copying that SQLite database into the test target.
3. Asserting that the contents of that SQLite database migrated as expected.

> Before copying your SQLite database, it's important to ensure it is, in fact, populated with test data. As we discussed above, Core Data uses `Write-Ahead Logging` to improve performance, so your data could be residing in the `sqlite-wal` file rather than the `sqlite` file. The easiest way to force any uncommitted changes is to *fake* a migration - add a breakpoint just after the `forceWALCheckpointingForStore(at:)` method, open the `Application Support` folder, copy the `sqlite` file and then abort the migration.

```
class CoreDataMigratorTests: XCTestCase {
    // 1
    var sut: CoreDataMigrator!

    // 2
    override class func setUp() {
        super.setUp()

        FileManager.clearTempDirectoryContents()
    }

    // 3
    override func setUp() {
        super.setUp()

        sut = CoreDataMigrator()
    }

    // 4
    override func tearDown() {
        sut = nil

        super.tearDown()
    }

    // 5
    func tearDownCoreDataStack(context: NSManagedObjectContext) {
        context.destroyStore()
    }

    // 6
    func test_givenVersion1Store_whenMigratedToVersion2_thenStoreIsCompatibleWithVersion2() throws {
        let sourceURL = FileManager.moveFileFromBundleToTempDirectory(filename: "CoreDataMigration_Example_1.sqlite")
        let toVersion = CoreDataMigrationVersion.version2

        try sut.migrateStore(at: sourceURL,
                             toVersion: toVersion)

        XCTAssertTrue(FileManager.default.fileExists(atPath: sourceURL.path))

        let model = try NSManagedObjectModel.managedObjectModel(forResource: toVersion.rawValue)
        let context = NSManagedObjectContext(model: model, storeURL: sourceURL)
        let request = NSFetchRequest.init(entityName: "Post")
        let sort = NSSortDescriptor(key: "postID", ascending: false)
        request.sortDescriptors = [sort]

        let migratedPosts = try? context.fetch(request)

        XCTAssertEqual(migratedPosts?.count, 10)

        let firstMigratedPost = migratedPosts?.first

        let migratedDate = firstMigratedPost?.value(forKey: "date") as? Date
        let migratedHexColor = firstMigratedPost?.value(forKey: "hexColor") as? String
        let migratedPostID = firstMigratedPost?.value(forKey: "postID") as? String
        let migratedContent = firstMigratedPost?.value(forKey: "content") as? String

        XCTAssertEqual(migratedDate?.timeIntervalSince1970, 1547494150.058821)
        XCTAssertEqual(migratedHexColor, "1BB732")
        XCTAssertEqual(migratedPostID, "FFFECB21-6645-4FDD-B8B0-B960D0E61F5A")
        XCTAssertEqual(migratedContent, "Test body")

        tearDownCoreDataStack(context: context)
    }
}
```

> There is no need to test every object stored in the persistent store; rather, we just have to assert that each entity has the correct number of objects, and then select one object per entity and assert the values on that object.

1. The [subject under test](https://en.wikipedia.org/wiki/System%5Funder%5Ftest?ref=williamboles.com) is a `CoreDataMigrator` instance.
2. The class-level `setUp` clears the temp directory once before the entire test suite runs, removing any leftover files from previous test runs.
3. The instance-level `setUp` creates a fresh `CoreDataMigrator` before each test.
4. `tearDown` nils out the migrator after each test.
5. `tearDownCoreDataStack(context:)` properly tears down the Core Data stack by destroying the store through the context. Simply deleting the SQLite files from disk without doing this would trigger a `database integrity compromised` error, as the store would be removed from under an active Core Data stack.
6. The test migrates a pre-populated `version1` SQLite database to `version2`, then verifies the migration by loading the store using the `version2` model and asserting that the data came through correctly. An interesting point to note is that rather than making use of the `Post` subclass of `NSManagedObject`, the above test uses a plain `NSManagedObject` instance and [KVC](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/index.html?ref=williamboles.com) to determine if the migration was a success. This is to handle the very likely scenario that the `Post` structure defined in the `version2` model will not be the final `Post` structure. If we used `Post` instances, then as the `Post` entity changed in later versions of the model, those changes would be mirrored in `Post` `NSManagedObject` subclass, which would result in this test potentially breaking. By using plain `NSManagedObject` instances and `KVC`, it is possible to ensure that this test is 100% accurate to the structure of the `Post` entity as defined in the `version2` model.

In the above test class, a few extensions are being used to make things easier.

Let's start with the extensions of `FileManager`:

```
extension FileManager {

    // 1
    static func clearTempDirectoryContents() {
        let tmpDirectoryContents = try! FileManager.default.contentsOfDirectory(atPath: NSTemporaryDirectory())
        tmpDirectoryContents.forEach {
            let tmpDirectory = URL(fileURLWithPath: NSTemporaryDirectory(),
                                   isDirectory: true)
            let fileURL = tmpDirectory.appendingPathComponent($0)
            try? FileManager.default.removeItem(atPath: fileURL.path)
        }
    }

    // 2
    static func moveFileFromBundleToTempDirectory(filename: String) -> URL {
        let tmpDirectory = URL(fileURLWithPath: NSTemporaryDirectory(),
                               isDirectory: true)
        let destinationURL = tmpDirectory.appendingPathComponent(filename)
        try? FileManager.default.removeItem(at: destinationURL)
        let bundleURL = Bundle(for: CoreDataMigratorTests.self).resourceURL!.appendingPathComponent(filename)
        try? FileManager.default.copyItem(at: bundleURL,
                                          to: destinationURL)

        return destinationURL
    }
}
```

1. `clearTempDirectoryContents()` removes all files from the temp directory, ensuring a clean slate before the test suite runs.
2. `moveFileFromBundleToTempDirectory(filename:)` copies a pre-populated SQLite file from the test bundle into the temp directory so that it can be migrated during a test. Any existing file at the destination is removed first to avoid conflicts.

Now, let's see the extensions for `NSManagedObjectContext`:

```
extension NSManagedObjectContext {

    // 1
    convenience init(model: NSManagedObjectModel,
                     storeURL: URL) {
        let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
        try! persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
                                                           configurationName: nil,
                                                           at: storeURL,
                                                           options: nil)

        self.init(concurrencyType: .mainQueueConcurrencyType)

        self.persistentStoreCoordinator = persistentStoreCoordinator
    }

    // 2
    func destroyStore() {
        persistentStoreCoordinator?.persistentStores.forEach {
            try? persistentStoreCoordinator?.remove($0)
            try? persistentStoreCoordinator?.destroyPersistentStore(at: $0.url!,
                                                                    ofType: $0.type,
                                                                    options: nil)
        }
    }
}
```

1. A convenience initialiser that creates a managed object context wired up to a specific model and store URL. This allows the tests to load a migrated store using the destination model version without needing a full `NSPersistentContainer` setup.
2. `destroyStore()` tears down the Core Data stack by first removing each persistent store from the coordinator and then destroying the underlying SQLite files. This ensures a clean teardown without triggering the `database integrity compromised` error that would result from simply deleting the files from disk.

> As this extension is in the test target, I'm happy to use `try!` and allow this code to crash if an error is thrown.

#### Migrating to Version 3

After another successful release, we decide to expand our posting functionality by allowing the user to add multiple sections to a post. These sections will be stored alongside the post in Core Data. As with any model change, we need to create a new model version.

Each section consists of:

- A title.
- A body.
- An index.

Which in turn reduces a post to:

- A unique ID.
- A random associated colour represented as a hex string.
- The date the post was created.
- A collection of sections.

Such that:

![Screenshot-showing-section-entity-1](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/01/Screenshot-showing-section-entity-1.png)

Migrating from `version2` to `version3` is slightly trickier than the previous migration, as `version3` splits an existing entity into two and creates a relationship between them. This will require implementing both a mapping model and a migration policy.

> To create a mapping model, open the `File` menu on the top bar, then click on `New File->New`. In the window that opens, scroll down to the `Core Data` section and double-tap on `Mapping Model`. This will open a wizard where you can select your source and destination model versions, so in this case: `CoreDataMigration_Example 2` and `CoreDataMigration_Example 3`. After that, you need to give the mapping a name and save it. I tend to follow `Migration[sourceVersion]to[destinationVersion]ModelMapping` as a naming convention, so `Migration2to3ModelMapping`.

A mapping model defines the transformations required to migrate from the source model to the destination model. In Xcode, a mapping model is an `xcmappingmodel` file that, when opened, has a GUI that's very similar to the Core Data Model GUI. A mapping model handles mapping between entities, attributes and relationships. The mapping model GUI even allows for simple transformations. If the model had a percentage attribute whose value used to range from `0 to 100` but now needs to range from `0 to 1`, we could use the `Expression` field on that attribute to perform this transformation by setting the expression to: `$source.percentage/100`. Despite the range of transformations possible within the mapping model GUI, some changes are just too complex and require a more custom approach - this is handled by creating a migration policy. A migration policy is an `NSEntityMigrationPolicy` subclass that defines how to map between two entities from two different model versions using the full Core-Data/Swift toolkit.

Migrating from `CoreDataMigration_Example 2` to `CoreDataMigration_Example 3` will require a custom migration policy as we will need to move the current `content` attribute's value on `Post` to both the `title` and `body` attributes on a newly created `Section` instance.

> As a rule of thumb: if a migration changes the shape of the data rather than just the names of its fields, you're probably in migration-policy territory. If you're splitting an entity, merging entities, or distributing one entity's data across several, you should be reaching for `NSEntityMigrationPolicy`.

```
// 1
final class Post2ToPost3MigrationPolicy: NSEntityMigrationPolicy {

    // 2
    override func createDestinationInstances(forSource sourceInstance: NSManagedObject,
                                             in mapping: NSEntityMapping,
                                             manager: NSMigrationManager) throws {
        try super.createDestinationInstances(forSource: sourceInstance,
                                             in: mapping,
                                             manager: manager)

        // 3
        guard let destinationPost = manager.destinationInstances(forEntityMappingName: mapping.name,
                                                                 sourceInstances: [sourceInstance]).first else {
            fatalError("Expected a post")
        }

        // 4
        let sourceBody = sourceInstance.value(forKey: "content") as? String
        let sourceTitle = sourceBody?.prefix(4).appending("...")

        // 5
        let section = NSEntityDescription.insertNewObject(forEntityName: "Section",
                                                          into: destinationPost.managedObjectContext!)
        section.setValue(sourceTitle,
                         forKey: "title")
        section.setValue(sourceBody,
                         forKey: "body")
        section.setValue(destinationPost,
                         forKey: "post")
        section.setValue(0,
                         forKey: "index")

        // 6
        var sections = Set<NSManagedObject>()
        sections.insert(section)

        destinationPost.setValue(sections,
                                 forKey: "sections")
    }
}
```

> Just like with mapping models, I have a naming convention for migration policies: `[Entity][Version]To[Entity][Version]MigrationPolicy`. This way, I can know at a glance exactly what the migration policy is doing.

1. `Post2ToPost3MigrationPolicy` is an `NSEntityMigrationPolicy` subclass that handles the custom migration from the `version2` `Post` entity to the `version3` `Post` and `Section` entities.
2. `createDestinationInstances(forSource:in:manager:)` is overridden to allow us to customise how `version2` `Post` instances are transformed into `version3` instances. The call to `super` creates the destination `Post` using the mapping rules defined in the mapping model.
3. The newly created destination `Post` is retrieved from the migration manager.
4. The `content` value is read from the source `Post` using `KVC`, and a `title` is derived by taking the first four characters and appending `...` \- a rough approximation since the old model had no concept of a title.
5. A new `Section` instance is created in the destination context and populated with the title, body, a reference back to the destination post, and an index of `0`.
6. The `sections` relationship is set on the destination post, linking the two entities together.

For this migration policy to be used during the migration, we need to add it to the mapping model by setting the `Custom Policy` on the `PostToPost` entity mapping:

![Screenshot showing Custom Policy set 'CoreDataMigration_Example.Post2ToPost3MigrationPolicy'](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/01/Screenshot-showing-custom-policy-3.png)

> It's important to note that the migration policy class name is prefixed with the module name.

All that's left to do is to update `CoreDataMigrationVersion` by introducing a `version3` case and updating `nextVersion`:

```
enum CoreDataMigrationVersion: String, CaseIterable {
    case version1 = "CoreDataMigration_Example"
    case version2 = "CoreDataMigration_Example 2"
    case version3 = "CoreDataMigration_Example 3"

    // Omitted unchanged functionality

    func nextVersion() -> CoreDataMigrationVersion? {
        switch self {
        case .version1:
            return .version2
        case .version2:
            return .version3
        case .version3:
            return nil
        }
    }
}
```

And that's it - we now have a migration path from not only `CoreDataMigration_Example 2` to `CoreDataMigration_Example 3` but also from `CoreDataMigration_Example` to `CoreDataMigration_Example 3`.

> Check out [CoreDataMigratorTests](https://github.com/wibosco/CoreDataMigrationRevised-Example/blob/main/CoreDataMigration-ExampleTests/Tests/CoreDataMigratorTests.swift?ref=williamboles.com) for the unit test that supports this migration.

#### Migrating to Version 4

The success of `Colourful Posts` knows no bounds, and we decide to release our next killer feature: deleting posts. This deletion functionality is actually a soft delete, which means that the post will still exist in Core Data but won't be shown to the user. We can achieve this by adding a new attribute to the `Post` entity - `softDelete`. Of course, this change will require a new model version and for us to handle the migration to that version. This migration can be handled as a Lightweight migration and, in fact, requires very little effort on our part. We only need to add a new case to `CoreDataMigrationVersion` and update `nextVersion()`:

```
enum CoreDataMigrationVersion: String, CaseIterable {
    case version1 = "CoreDataMigration_Example"
    case version2 = "CoreDataMigration_Example 2"
    case version3 = "CoreDataMigration_Example 3"
    case version4 = "CoreDataMigration_Example 4"

    // Omitted unchanged functionality

    func nextVersion() -> CoreDataMigrationVersion? {
        switch self {
        case .version1:
            return .version2
        case .version2:
            return .version3
        case .version3:
            return .version4
        case .version4:
            return nil
        }
    }
}
```

> Most migrations will probably look like this - a new enum case and a new switch arm. No custom migration code required.

## We Got There 🏁

The default Core Data migration approach required `n-1` new mappings for every new model version - a burden that grows with every release and tempts us into dropping support for older versions. With the above step-by-step approach, adding support for migrating to a new version only costs us a single step. Breaking the migration work down into a series of small steps takes a lot of the difficulty out of migrations.

And remember, if that's how someone on the bus reacts to having to move their bag, we should try and spare this person the trauma that losing their unlocked *thingamabobs* would surely cause 😉.

To see the complete working example, visit the [repository](https://github.com/wibosco/CoreDataMigrationRevised-Example?ref=williamboles.com) and clone the project.

> Note that the code in the repository is [unit tested](https://williamboles.com/testing-through-different-lenses/), so it isn't 100% the same as the code snippets shown in this post. The changes mainly involve a greater use of protocols to allow for test-doubles to be [injected](https://williamboles.com/let-dependency-injection-lead-you-to-a-better-design/) into the various types. While not hugely different, I didn't want to present the increased complexity necessary for unit testing to get in the way of the core message in this post.

---

I want to acknowledge that I leaned on the most excellent `Core Data` book by [Florian Kugler](https://twitter.com/floriankugler?lang=en&ref=williamboles.com) and [Daniel Eggert](https://twitter.com/danielboedewadt?lang=en&ref=williamboles.com), which you can get [here](https://www.objc.io/books/core-data/?ref=williamboles.com). I highly recommend that you give that book a read, as it's a treasure trove of Core Data knowledge.