Schema Migration
Schema Migration
Section titled “Schema Migration”Schema migration is the process of updating your Core Data model as your app evolves. Core Data provides three migration strategies: lightweight, staged (iOS 17+), and deferred (iOS 14+).
When Migration is Required
Section titled “When Migration is Required”Core Data refuses to open a store when the model doesn’t match:
Error: NSPersistentStoreIncompatibleVersionHashErrorThis means: Your data model changed, and you need to migrate.
Lightweight Migration (Recommended)
Section titled “Lightweight Migration (Recommended)”Lightweight migration is automatic and handles most common changes.
Enabling Lightweight Migration
Section titled “Enabling Lightweight Migration”With NSPersistentContainer (automatic):
let container = NSPersistentContainer(name: "Model")// Lightweight migration enabled by defaultWith NSPersistentStoreDescription (automatic):
let description = NSPersistentStoreDescription(url: storeURL)// Lightweight migration enabled by defaultManual setup (if needed):
let options = [ NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]try coordinator.addPersistentStore( ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)Supported Operations
Section titled “Supported Operations”Attributes:
- Add attribute
- Remove attribute
- Make optional attribute non-optional (with default value)
- Make non-optional attribute optional
- Rename attribute (using renaming identifier)
Relationships:
- Add relationship
- Remove relationship
- Rename relationship (using renaming identifier)
- Change cardinality (to-one ↔ to-many)
- Change ordering (ordered ↔ non-ordered)
Entities:
- Add entity
- Remove entity
- Rename entity (using renaming identifier)
- Create parent/child entity
- Move attributes up/down hierarchy
- Move entities in/out of hierarchy
Cannot do:
- Merge entity hierarchies (entities without common parent can’t share parent)
Renaming Attributes/Entities
Section titled “Renaming Attributes/Entities”Set the renaming identifier to the old name:
// In Data Model Editor:// 1. Rename attribute from "color" to "paintColor"// 2. Set Renaming Identifier to "color"This allows chaining renames across versions:
- V1:
color - V2:
paintColor(renaming ID:color) - V3:
primaryColor(renaming ID:paintColor)
Migration works: V1→V2, V2→V3, and V1→V3.
Testing Lightweight Migration
Section titled “Testing Lightweight Migration”// Check if migration is possiblelet sourceModel = // ... load V1 modellet destinationModel = // ... load V2 model
if let mappingModel = try? NSMappingModel.inferredMappingModel( forSourceModel: sourceModel, destinationModel: destinationModel) { print("Lightweight migration possible")} else { print("Lightweight migration not possible")}Composite Attributes (iOS 17+)
Section titled “Composite Attributes (iOS 17+)”New in iOS 17: Structured data within a single attribute.
Creating Composite Attributes
Section titled “Creating Composite Attributes”In Data Model Editor:
- Add Composite Attribute
- Add elements (String, Int, Date, etc.)
- Can nest composite attributes
// Example: ColorScheme composite// - primary: String// - secondary: String// - tertiary: String
class Aircraft: NSManagedObject { @NSManaged var colorScheme: [String: Any]}
// Usageaircraft.colorScheme = [ "primary": "Red", "secondary": "White", "tertiary": "Blue"]
// QueryingfetchRequest.predicate = NSPredicate(format: "colorScheme.primary == %@", "Red")Benefits
Section titled “Benefits”- No transformable code needed
- Supports predicates with keypaths
- Better than flattened attributes
- Can prevent faulting across relationships
Staged Migration (iOS 17+)
Section titled “Staged Migration (iOS 17+)”For complex migrations that exceed lightweight capabilities.
When to Use
Section titled “When to Use”- Changes don’t fit lightweight patterns
- Need to run custom code during migration
- Need to decompose complex changes into steps
Key Classes
Section titled “Key Classes”NSStagedMigrationManager- Manages migration event loopNSCustomMigrationStage- Custom code executionNSLightweightMigrationStage- Lightweight-eligible changesNSManagedObjectModelReference- Model references with checksums
Example: Denormalizing Data
Section titled “Example: Denormalizing Data”Problem: Move flightData attribute to separate entity.
Solution: Decompose into stages:
Stage 1 (Lightweight): Add new entity and relationship
// ModelV1 → ModelV2// Add FlightData entity// Add flightParameters relationship to AircraftStage 2 (Custom): Copy data
- Fetch rows using generic
NSManagedObject/NSFetchRequestResulttypes. - Create new entities and copy data inside the migration stage handler.
- Ensure the custom logic is restartable if the process is interrupted.
Stage 3 (Lightweight): Remove old attribute
// ModelV3 → ModelV4// Remove flightData attribute from AircraftGetting Version Checksum
Section titled “Getting Version Checksum”From Xcode build log:
Compile data model Model.xcdatamodeldversion checksum: ABC123...Deferred Migration (iOS 14+)
Section titled “Deferred Migration (iOS 14+)”Defer cleanup work to keep app responsive.
When to Use
Section titled “When to Use”- Removing attributes/relationships
- Changing relationship hierarchy
- Changing relationship ordering
- Any migration with expensive cleanup
How It Works
Section titled “How It Works”- Migration runs synchronously (fast)
- Cleanup (indices, column drops) is deferred
- App uses latest schema immediately
- Finish cleanup when resources available
Enabling Deferred Migration
Section titled “Enabling Deferred Migration”let description = NSPersistentStoreDescription(url: storeURL)description.setOption( true as NSNumber, forKey: NSPersistentStoreDeferredLightweightMigrationOptionKey)Checking for Pending Work
Section titled “Checking for Pending Work”let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( ofType: NSSQLiteStoreType, at: storeURL)
if let hasDeferredWork = metadata[NSPersistentStoreDeferredLightweightMigrationOptionKey] as? Bool, hasDeferredWork { print("Deferred migration work pending")}Finishing Deferred Migration
Section titled “Finishing Deferred Migration”func finishDeferredMigration() { let coordinator = container.persistentStoreCoordinator
do { try coordinator.finishDeferredLightweightMigration() print("Deferred migration completed") } catch { print("Failed to finish deferred migration: \(error)") }}Scheduling with Background Tasks
Section titled “Scheduling with Background Tasks”import BackgroundTasks
// Register taskBGTaskScheduler.shared.register( forTaskWithIdentifier: "com.example.app.migration", using: nil) { task in self.handleMigrationTask(task as! BGProcessingTask)}
// Schedule taskfunc scheduleMigration() { let request = BGProcessingTaskRequest(identifier: "com.example.app.migration") request.requiresNetworkConnectivity = false request.requiresExternalPower = false
try? BGTaskScheduler.shared.submit(request)}
// Handle taskfunc handleMigrationTask(_ task: BGProcessingTask) { task.expirationHandler = { task.setTaskCompleted(success: false) }
finishDeferredMigration() task.setTaskCompleted(success: true)}Migration Debugging
Section titled “Migration Debugging”Enable Migration Debug
Section titled “Enable Migration Debug”-com.apple.CoreData.MigrationDebug 1Output:
CoreData: annotation: Migration: Migrating from version 1 to version 2CoreData: annotation: Migration: Inferred mapping modelCoreData: annotation: Migration: Completed successfullyCommon Errors
Section titled “Common Errors”NSPersistentStoreIncompatibleVersionHashError
- Model changed, migration required
- Enable lightweight migration or create mapping model
NSMigrationMissingSourceModelError
- Can’t find source model
- Ensure all model versions are in bundle
NSMigrationError
- Migration failed
- Check if changes are lightweight-compatible
- Use staged migration for complex changes
Best Practices
Section titled “Best Practices”- Test migrations thoroughly - Test upgrade paths from all previous versions
- Keep model versions - Don’t delete old .xcdatamodel files
- Use lightweight when possible - Simplest and most reliable
- Decompose complex changes - Use staged migration for non-lightweight changes
- Defer expensive cleanup - Use deferred migration for large datasets
- Version your models - Create new model version for each release
- Test on real data - Migration behavior differs with large datasets
- Document changes - Keep migration notes for future reference
Testing Migrations
Section titled “Testing Migrations”func testMigration() throws { // 1. Create store with old model let oldModelURL = Bundle.main.url(forResource: "ModelV1", withExtension: "momd")! let oldModel = NSManagedObjectModel(contentsOf: oldModelURL)!
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: oldModel) try coordinator.addPersistentStore( ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil )
// 2. Add test data let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.persistentStoreCoordinator = coordinator
let entity = NSEntityDescription.insertNewObject(forEntityName: "Article", into: context) entity.setValue("Test", forKey: "name") try context.save()
// 3. Close store try coordinator.remove(coordinator.persistentStores.first!)
// 4. Migrate with new model let newModelURL = Bundle.main.url(forResource: "ModelV2", withExtension: "momd")! let newModel = NSManagedObjectModel(contentsOf: newModelURL)!
let newCoordinator = NSPersistentStoreCoordinator(managedObjectModel: newModel) let options = [ NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true ] try newCoordinator.addPersistentStore( ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options )
// 5. Verify data let newContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) newContext.persistentStoreCoordinator = newCoordinator
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Article") let results = try newContext.fetch(fetchRequest)
XCTAssertEqual(results.count, 1) XCTAssertEqual(results.first?.value(forKey: "name") as? String, "Test")}Summary
Section titled “Summary”- Use lightweight migration - Handles most common changes automatically
- Enable by default - NSPersistentContainer enables it automatically
- Use renaming identifiers - For renaming attributes/entities/relationships
- Use composite attributes (iOS 17+) - For structured data
- Use staged migration (iOS 17+) - For complex, non-lightweight changes
- Use deferred migration (iOS 14+) - For expensive cleanup operations
- Test thoroughly - Verify all upgrade paths
- Keep all model versions - Required for migration
- Enable migration debug - Helps diagnose issues
- Document changes - Track what changed in each version