How to perform migrations in SwiftData

SwiftData is a framework that allows you to work with local data using Swift-like API. Migrations are the process of updating your data model and the underlying data when you make changes to your app.

SwiftData can perform two types of migrations: lightweight and complex.

  • Lightweight migrations are automatic and can handle small changes, such as adding, removing, or renaming properties or entities, changing the type or delete rule of a relationship, or adding the .unique where it previously wasn’t, .externalStorage, or .allowsCloudEncryption attributes.
  • Complex migrations require manual intervention and can handle more advanced changes, such as transforming data values, merging or splitting entities, or changing the inheritance hierarchy.

To perform migrations in SwiftData, you need to follow these steps:

  • Define multiple versions of your data model and annotate them with the @Model macro.
  • Define an enum conforming to VersionedSchema and put your models inside the enum (used as a namespace).
  • Create another enum that conforms to the SchemaMigrationPlan protocol and define the schemas and stages for the migration.
  • Create a custom ModelContainer configuration that uses the migration plan and pass it to the model container modifier in your app entry point.

After migration, thoroughly verify data accuracy and completeness in the target system. Conduct comprehensive testing in a non-production environment to validate data integrity and functionality after migration.

Here’s an example from Model your schema with SwiftData (WWDC 2023):

import SwiftUI
import SwiftData

//  Defining versioned schemas
enum SampleTripsSchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        var name: String
        var destination: String
        var start_date: Date
        var end_date: Date
    
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }

    // Define the other models in this version...
}

enum SampleTripsSchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        var start_date: Date
        var end_date: Date
    
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }

    // Define the other models in this version...
}

enum SampleTripsSchemaV3: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        @Attribute(originalName: "start_date") var startDate: Date
        @Attribute(originalName: "end_date") var endDate: Date
    
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }

    // Define the other models in this version...
}

// Implementing a SchemaMigrationPlan
enum SampleTripsMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
    }
    
    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV1.self,
        toVersion: SampleTripsSchemaV2.self,
        willMigrate: { context in
            let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
                      
            // De-duplicate Trip instances here...
                      
            try? context.save() 
        }, didMigrate: nil
    )
  
    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self
    )
}

// Configuring the migration plan
struct TripsApp: App {
    let container = ModelContainer(
        for: Trip.self, 
        migrationPlan: SampleTripsMigrationPlan.self
    )
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}