Overview
A migration updates the on-disk store to a new model version without losing user data. A released app that cannot open its own store crashes at launch, so treat every model change as a migration. Stack setup is in core-data; for SwiftData schema evolution see swiftdata.
Enable automatic lightweight migration
Set both flags on the store description before loading stores. Core Data then infers the mapping for supported changes.
let description = container.persistentStoreDescriptions.first!
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = trueMigration runs inside loadPersistentStores, so load asynchronously for large stores (core-data).
Add a model version for every shipped structural change
In Xcode choose Editor > Add Model Version. Never edit a version that any user has installed. Set the new version as Current in the model inspector; Core Data reads it as the target schema.
Model.xcdatamodeld/
Model.xcdatamodel v1, never edit
Model v2.xcdatamodel additive
Model v3.xcdatamodel current
Know what needs more than lightweight
Lightweight migration covers additive and rename-style changes: adding or removing attributes and relationships, changing optionality (with a default when going to required), and renaming an entity or attribute through its renaming identifier. Splitting or merging entities, transforming values or types, and deriving a value from several attributes need custom logic.
Use staged migration to mix lightweight and custom steps
Staged migration (iOS 17+) lets you describe a sequence of stages with NSStagedMigrationManager: NSLightweightMigrationStage for inferable hops and NSCustomMigrationStage for hops that need your code. Prefer it for new work over relying on a single large mapping model, because each hop is small and testable. The types are documented under “Staged migrations” in Apple’s Core Data documentation.
Write a mapping model for value transformations
For an entity-level transformation, add a Mapping Model (File > New > Mapping Model), map each source entity, and subclass NSEntityMigrationPolicy for custom logic. Set the class name under “Custom Policy” in the mapping inspector.
final class PersonToContactPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource source: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager
) throws {
let contact = NSEntityDescription.insertNewObject(
forEntityName: "Contact", into: manager.destinationContext)
contact.setValue(source.value(forKey: "firstName"), forKey: "givenName")
manager.associate(sourceInstance: source, withDestinationInstance: contact, for: mapping)
}
}Test every hop against a fixture store
Commit one binary store for each released schema version, copy it to a temp URL, load it with the current model and the migration flags, and assert that load succeeds and the data survived. Generate fixtures by running the shipped version against a known dataset and copying its .sqlite, -wal, and -shm files. Run these in CI.
let container = NSPersistentContainer(name: "Model")
container.persistentStoreDescriptions.first!.url = tempURL
var loadError: Error?
container.loadPersistentStores { _, error in loadError = error }
XCTAssertNil(loadError)Keep a path from every shipped version
Core Data can chain v1 to v3 through v2, but every hop needs an inferable or explicit mapping, so keep each one tested. Never skip a version that users may still run. If supporting a very old version is impractical, ship a recovery flow that exports the data before recreating the store.