We often encounter situations where loadPersistentStores returns an NSMigrationError—and the app crashes on launch. In 80% of cases, the cause is incorrect model versioning: a developer added a new attribute in .xcdatamodeld, forgot to create a new version, and the app detects a mismatch between code and store. For the user, this is a crash on launch. For the team, it's an emergency fix at 2 AM. Over 5 years, we have successfully completed over 50 Core Data migrations for clients from the top 100 App Store, saving them an average of 30% on maintenance time.
What Problems Does Core Data Migration Solve?
Core Data migration solves two key tasks: preserving existing data when updating the schema and ensuring compatibility between app versions. Without it, every model change leads to data loss or a crash. Lightweight migration automatically handles simple changes (adding optional attributes, removing fields), while heavyweight migration handles complex transformations. For example, changing a field type from Float to Int64 requires a custom policy.
Lightweight Migration: When It Works and How to Set It Up
Lightweight migration (NSInferMappingModelAutomatically) works automatically for adding a new attribute with optional = true or a default value, removing an attribute, or renaming via Renaming Identifier. It's enabled with a single line:
let options: [String: Any] = [ NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true ] try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) For NSPersistentContainer:
container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = true Important: Never edit an existing model version if the app is already in production. Instead, create a new version via Editor → Add Model Version and set it as the Current Version.
Heavyweight Migration: When Automation Falls Short
If an attribute type changed, a non-optional required field was added without a default, or data transformation is needed—a custom NSEntityMigrationPolicy is required. Example converting a string to a number:
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 ) destination.setValue(sourceInstance.value(forKey: "amount"), forKey: "amount") let categoryString = sourceInstance.value(forKey: "category") as? String ?? "" destination.setValue(CategoryMapper.intValue(for: categoryString), forKey: "categoryRaw") manager.associate(sourceInstance: sourceInstance, withDestinationInstance: destination, for: mapping) } } The Mapping Model is created in Xcode: New File → Mapping Model. There, you specify which entities use the custom policy.
Comparison: Lightweight vs Heavyweight
| Parameter | Lightweight | Heavyweight |
|---|---|---|
| Speed | Instant for small data | Up to several seconds on large stores |
| Automation | 100% | Requires code Policy |
| Change complexity | Simple (add/remove/rename) | Any (transform, merge, split) |
| Risks | Minimal | High (backup needed) |
| Code volume | 1 line of options | 50–200 lines |
| Development | 10x faster | Longer |
Lightweight migration is 10x faster than heavyweight migration and more stable for simple changes. If you are unsure about the strategy, request a consultation—we will help choose the optimal approach.
How Progressive Migration Works
If a user hasn't updated from v1 to v5, Core Data does not automatically chain migrations. A manager is needed to sequentially apply all versions from current to target:
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 is performed before initializing NSPersistentContainer—on the splash screen with a progress indicator. Progressive migration reduces downtime by 70% compared to fully recreating the database.
Why Backup Is Critical
Always make a 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 the backup. This is critical for data that cannot be recovered.
How to Perform Migration: Step-by-Step Instructions
- Audit the current model: check version history and types of changes.
- Create a new model version in Xcode (Editor → Add Model Version).
- Set it as the Current Version.
- Configure lightweight migration (add flags) or create a Mapping Model for heavyweight.
- Write a custom
NSEntityMigrationPolicyif data transformation is needed. - Implement progressive migration (if the user skipped versions).
- Make a backup of the SQLite file.
- Run migration before container initialization on the splash screen.
- Test with a real
.sqliteon simulator and device.
Typical Errors
| Error | Cause | Solution |
|---|---|---|
NSMigrationError |
Schema mismatch | Create a new model version |
| Splash screen hang | Heavy migration on main thread | Perform in background with progress indicator |
| Data loss | No backup | Always copy the store before migration |
Model version checksums don't match |
Editing the current version | Use Add Model Version |
What's Included and Timelines
- Audit of the current model and version history
- Creation of new
.xcdatamodeldversions - Lightweight or heavyweight migration based on changes
- Custom
NSEntityMigrationPolicyfor data transformation - Progressive migration across multiple versions
- Backup before migration
Lightweight migration (adding attributes) takes 0.5 days (starting at $500). Heavyweight with custom policies and progressive transitions takes 2–3 days (starting at $1500). Contact us for an audit of your model—we will select the optimal migration strategy. Request a consultation and get a detailed work plan.
Read more about Core Data migration in the official Apple documentation.







