Core Data Setup in iOS Applications
Core Data is not just SQLite wrapper. It's object graph with lazy loading, caching, change tracking, and CloudKit sync capability. Properly configured it speeds up local data work. Improperly — causes deadlocks and crashes on NSFetchedResultsController.
Stack Configuration
Starting iOS 10, recommended way is NSPersistentContainer. Encapsulates NSManagedObjectModel, NSPersistentStoreCoordinator, and main NSManagedObjectContext.
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error { fatalError("Core Data store failed: \(error)") }
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
automaticallyMergesChangesFromParent = true is critical. Without this, changes saved in background context don't automatically reach viewContext, and NSFetchedResultsController doesn't update UI.
Multithreading — Main Trap
NSManagedObject not thread-safe. Can't pass object between threads — only objectID via NSManagedObjectID. In background context get object copy:
let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.perform {
let objectInBg = backgroundContext.object(with: objectID)
// change objectInBg
try? backgroundContext.save()
}
Most common crash: EXC_BAD_ACCESS or NSInternalInconsistencyException on accessing NSManagedObject not in its thread. Instruments → Core Data template shows where this happens.
performAndWait vs perform. perform is async, performAndWait is sync and can deadlock if called from main thread waiting for background context which in turn waits for main. Use perform for background saves.
NSFetchedResultsController and Diffable Data Source
NSFetchedResultsController tracks Core Data changes and notifies delegate. Binding with UICollectionViewDiffableDataSource works via controllerDidChangeContent:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
var snapshot = NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>()
snapshot.appendSections([.main])
snapshot.appendItems(controller.fetchedObjects?.map(\.objectID) ?? [])
dataSource.apply(snapshot, animatingDifferences: true)
}
Use objectID in snapshot, not NSManagedObject itself — otherwise diffable source can't properly compare objects.
Migrations
On model data change migration needed. Easy migration (NSInferMappingModelAutomatically) works for adding/removing attributes. For renames, type changes — custom migration policy via NSEntityMigrationPolicy. Without proper migration loadPersistentStores returns NSMigrationError, app won't start.
Add to configuration:
container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = true
CloudKit Synchronization
NSPersistentCloudKitContainer instead of NSPersistentContainer enables iCloud CloudKit sync. Requires: iCloud Entitlement, CloudKit capability in Xcode, model without some attribute types (Binary Data with External Storage doesn't auto-sync).
Sync conflicts resolved via mergePolicy — NSMergeByPropertyObjectTrumpMergePolicy usually right choice.
What's Included in Work
- Create
.xcdatamodeldwith entity and relationships - Configure
NSPersistentContainerwith right context parameters - Background context for data import and writes
-
NSFetchedResultsControllerfor UI data display - Migration strategy for future model changes
- Optional: CloudKit sync
Timeline
Basic stack with one-two entities and NSFetchedResultsController: 1 day. Complex model, migrations, background sync, CloudKit integration: 2–3 days. Cost estimated after data requirements analysis.







