Implementing Database Schema Migration (Core Data Migration) in iOS Applications
loadPersistentStores returned an NSMigrationError — and the app won't launch. This happens when a developer added a new attribute to .xcdatamodeld, forgot to create a new model version, and the app discovered a mismatch between code and storage. For users this is a crash on launch. For the team — urgent 2 AM fix.
Data Model Versioning
Core Data stores all model versions in xcdatamodeld package (a folder with *.xcdatamodel files inside). Active version is indicated in .xccurrentversion. When changing schema:
- In Xcode: Editor → Add Model Version
- Set new version as Current Version
- Describe the migration
Never edit an existing model version if the app is already in production — this guarantees a crash for all users.
Lightweight Migration — When It Works
Lightweight migration (NSInferMappingModelAutomatically) works automatically for:
- Adding new attribute with
optional = trueor withdefaultValue - Deleting attribute
- Renaming entity or attribute with
Renaming Identifier
Enabled with one line:
let options: [String: Any] = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
If Renaming Identifier in .xcdatamodeld is set correctly — Core Data builds mapping between versions itself. For NSPersistentContainer:
container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = true
Heavyweight Migration — When Automation Doesn't Work
If attribute type changed, required non-zero field without default added, or data transformation is needed during migration — need custom NSEntityMigrationPolicy.
class TransactionMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sourceInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let destination = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName!,
into: manager.destinationContext
)
// Copy attributes
destination.setValue(sourceInstance.value(forKey: "amount"), forKey: "amount")
// Transform: old String → new enum Int
let categoryString = sourceInstance.value(forKey: "category") as? String ?? ""
destination.setValue(CategoryMapper.intValue(for: categoryString), forKey: "categoryRaw")
manager.associate(sourceInstance: sourceInstance, withDestinationInstance: destination, for: mapping)
}
}
NSEntityMigrationPolicy is specified in MappingModel.xcmappingmodel — file created via Xcode: New File → Mapping Model. It describes mapping of entity from old version → entity in new version and which Policy class to use.
Progressive Migration Through Multiple Versions
If user hasn't updated app from v1 to v5 — Core Data doesn't automatically build migration chains. Need a manager:
class MigrationManager {
func migrateStore(at storeURL: URL) throws {
var currentURL = storeURL
while true {
guard let sourceModel = NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata(at: currentURL)),
let destinationModel = nextModel(after: sourceModel) else { break }
let mappingModel = try NSMappingModel.inferredMappingModel(
forSourceModel: sourceModel, destinationModel: destinationModel
)
let migrator = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel)
let tempURL = storeURL.appendingPathExtension("migration")
try migrator.migrateStore(from: currentURL, type: .sqlite, to: tempURL, type: .sqlite, mapping: mappingModel)
try FileManager.default.removeItem(at: currentURL)
try FileManager.default.moveItem(at: tempURL, to: storeURL)
}
}
}
Migration runs before NSPersistentContainer initialization — on splash screen with progress indicator.
Backup Before Migration
Always backup before heavyweight migration:
let backupURL = storeURL.deletingLastPathComponent()
.appendingPathComponent("backup_\(Date().timeIntervalSince1970).sqlite")
try FileManager.default.copyItem(at: storeURL, to: backupURL)
If migration fails — restore backup. Critical for non-recoverable data.
Typical Mistakes
- Editing current model version instead of creating new one —
Model version checksums don't match - Heavy migration on main thread — UI hangs for several seconds on large database
- Not testing migration with real
.sqlitefile — errors discovered only on user device
Work Scope
- Audit current model and version history
- Create new
.xcdatamodeldversions - Lightweight or heavyweight migration depending on changes
- Custom
NSEntityMigrationPolicyfor data transformation - Progressive migration through multiple versions
- Backup before migration
Timeline
Lightweight migration (adding attributes): 0.5 day. Heavyweight with custom policies and progressive transitions through several versions: 2–3 days.







