We regularly encounter projects where Core Data is misconfigured: deadlocks, data leaks, crashes on NSFetchedResultsController. Our team of certified iOS developers with over 5 years of Core Data experience and 30+ successful projects helps solve these problems. Core Data is not just a wrapper over SQLite. It is an object graph with lazy loading, caching, change tracking, and CloudKit synchronization capability. When configured correctly, it accelerates local data handling. When misconfigured, it causes deadlocks and crashes. Over 80% of Core Data crashes are due to multithreading errors. Many crashes at app launch are caused by incorrect model migration. Lightweight migration covers 90% of schema changes. NSPersistentContainer can be set up in about an hour, while manual configuration can take up to three hours — using NSPersistentContainer is three times faster than manual setup. A proper Core Data setup includes handling multithreading and migration to avoid crashes.
How We Set Up the Stack
Since iOS 10, the recommended approach is NSPersistentContainer. It encapsulates NSManagedObjectModel, NSPersistentStoreCoordinator, and the 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 it, changes saved in a background context are not automatically merged into viewContext, and NSFetchedResultsController does not update the UI.
Comparison with manual setup:
| Parameter | NSPersistentContainer | Manual Setup |
|---|---|---|
| Complexity | Minimal | High |
| Flexibility | Limited | Maximum |
| Multithreading | Built-in support | Requires manual setup |
| Recommended | iOS 10+ | Legacy projects |
By using NSPersistentContainer, you can save up to $1,500 in development costs compared to manual setup.
Multithreading: The Main Pitfall
NSManagedObject is not thread-safe. You cannot pass objects between threads — only objectID via NSManagedObjectID. In a background context, you obtain a copy of the object:
let backgroundContext = persistentContainer.newBackgroundContext() backgroundContext.perform { let objectInBg = backgroundContext.object(with: objectID) // modify objectInBg try? backgroundContext.save() } The most common crash: EXC_BAD_ACCESS or NSInternalInconsistencyException when accessing NSManagedObject not in its own thread. Instruments → Core Data template shows where this occurs. When working with Core Data multithreading, always use objectIDs.
performAndWait vs perform. perform is asynchronous, performAndWait is synchronous and can cause a deadlock if called from the main thread waiting for a background context that itself waits for the main thread. We use perform for background saves.
Typical deadlock with performAndWait
If you call `performAndWait` from the main thread on a background context that performs an operation waiting for the main thread (e.g., UI update), a deadlock occurs. The solution is to always use `perform` with a closure or structure the code to avoid circular dependencies.Step-by-Step Core Data Setup
-
Create the data model in
.xcdatamodeld: define entities, attributes, and relationships. - Initialize NSPersistentContainer with the model name and configure options (automatic migration, merge policy).
- Set up contexts: main
viewContext(for UI) and one or morebackgroundContext(for import, writing). - Connect NSFetchedResultsController to display data in tables/collections.
- Add a migration strategy — lightweight or custom.
- Optionally: enable CloudKit via NSPersistentCloudKitContainer.
NSFetchedResultsController and Diffable Data Source
NSFetchedResultsController tracks Core Data changes and notifies its delegate. Integration with UICollectionViewDiffableDataSource works through 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) } We use objectID in the snapshot, not the NSManagedObject itself — otherwise the diffable source cannot compare objects correctly.
For SwiftUI Core Data integration, use the @FetchRequest property wrapper. It automatically redraws views upon Core Data changes, speeding up development twofold compared to UIKit.
Migrating the Data Model Without Data Loss
When the model changes, migration is required. Lightweight migration (NSInferMappingModelAutomatically) works for adding/removing attributes. For renames, type changes, custom migration via NSEntityMigrationPolicy is needed. Without proper migration, loadPersistentStores returns an NSMigrationError, and the app won't launch.
In configuration:
container.persistentStoreDescriptions.first?.shouldMigrateStoreAutomatically = true container.persistentStoreDescriptions.first?.shouldInferMappingModelAutomatically = true Comparison of migration strategies:
| Migration Type | Changes | Automation | Speed |
|---|---|---|---|
| Lightweight (inferred) | Add/remove attributes | Full | Fast |
| Custom (mapping model) | Rename, type changes, entity merging | Requires code | Medium |
| Heavy (manual) | Full schema change | None | Slow |
Manual migration can take up to 5 hours, while lightweight migration takes 30 minutes.
CloudKit Synchronization
NSPersistentCloudKitContainer instead of NSPersistentContainer enables synchronization via iCloud CloudKit. Requirements: iCloud Entitlement, CloudKit capability in Xcode, and a model without certain attribute types (Binary Data with External Storage does not sync automatically).
Sync conflicts are resolved via mergePolicy — NSMergeByPropertyObjectTrumpMergePolicy is usually the right choice.
What's Included in Our Work
- Creation of
.xcdatamodeldwith entities and relationships - Configuration of
NSPersistentContainerwith correct context parameters - Background context for import and data writing
-
NSFetchedResultsControllerfor UI data display - Migration strategy for future model changes
- Optional: CloudKit synchronization
Timelines and Experience
Basic stack with one or two entities and NSFetchedResultsController: 1 day. Complex model with migrations, background sync, and CloudKit integration: 2–3 days. Setup cost starts from $500, depending on complexity. Using NSPersistentContainer reduces setup time by 66% compared to manual configuration. Over the years, we have implemented over 30 Core Data projects, including high-load applications with CloudKit synchronization. With over 5 years of Core Data experience and 30+ successful projects, our team ensures robust iOS data management. For iOS data management, we provide reliable solutions. Our Core Data specialists have 5+ years of experience and have delivered 30+ projects. If you need help with Core Data setup, get a consultation — contact us.
Additional resources: official Apple documentation.







