Build and maintain data persistence using Core Data for apps that have not adopted SwiftData. Covers stack setup, concurrency, batch operations, NSFetchedResultsController, persistent history tracking, staged migration, and testing.
Contents
- Stack Setup & Concurrency
- NSFetchedResultsController
- Batch Operations
- Persistent History Tracking
- Staged Migration & Coexistence
- Testing
- Common Mistakes
- Review Checklist
- References
Stack Setup & Concurrency
Core Data contexts are bound to queues: viewContext runs on the main queue, while background tasks use newBackgroundContext().
- Context Isolation: Always wrap access in
context.perform(_:)orcontext.performAndWait(_:). - Thread Crossing: Never pass
NSManagedObjectinstances between contexts or threads; passNSManagedObjectIDand re-fetch viaexistingObject(with:). - Automatic Merging: Set
automaticallyMergesChangesFromParent = trueand configuremergePolicy = NSMergeByPropertyObjectTrumpMergePolicyonviewContext.
import CoreData
func updateTrip(id: NSManagedObjectID, name: String) async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
guard let trip = try context.existingObject(with: id) as? CDTrip else { return }
trip.name = name
if context.hasChanges { try context.save() }
}
}
NSFetchedResultsController
Drives UI collections from an NSFetchRequest with automatic change tracking:
- Always supply at least one sort descriptor in the fetch request.
- Use diffable snapshots (
controller(_:didChangeContentWith:)) on iOS 13+ to animate UI changes. - Delete the cache with
deleteCache(withName:)before changing predicate or sort descriptors, or passcacheName: nil.
Batch Operations
NSBatchInsertRequest, NSBatchUpdateRequest, and NSBatchDeleteRequest execute directly at the SQLite level, bypassing managed object contexts.
- Set
resultType = .objectIDs/.resultTypeObjectIDs. - Merge results manually into live contexts using
NSManagedObjectContext.mergeChanges(fromRemoteContextSave:into:). - Note that batch deletes bypass Core Data relationship delete rules (e.g.
Deny).
Persistent History Tracking
Track store modifications across app extensions, widgets, and processes:
- Enable
NSPersistentHistoryTrackingKey: truein store options. - Query changes using
NSPersistentHistoryChangeRequestand merge them into local contexts. - Purge consumed transactions periodically with history truncation requests to prevent database bloat.
- Read references/persistent-history.md for full transaction tracking and purge loops.
Staged Migration & Coexistence
- Staged Migration (iOS 17+): Use
NSStagedMigrationManagerwith lightweight and custom stages (NSCustomMigrationStage) for deterministic upgrades. Read references/staged-migration.md. - SwiftData Boundary: When sharing a database file with SwiftData, point both to the same store URL, preserve model schemas and attribute names, and map renames with
@Attribute(originalName:). Route pure SwiftData work to theswiftdataskill.
Testing
Use NSInMemoryStoreType for deterministic unit testing. Share a single compiled NSManagedObjectModel instance across tests to prevent duplicate entity runtime errors.
Common Mistakes
- Passing NSManagedObject across threads: Causes concurrency crashes. Always pass
NSManagedObjectIDand refetch withexistingObject(with:). - Missing mergeChanges after batch requests: Batch operations bypass memory contexts. Always merge returned object IDs.
- Calling save() without hasChanges: Avoid redundant I/O; guard saves with
if context.hasChanges. - Omitting merge policy on viewContext: Missing
mergePolicycauses conflict save crashes. SetNSMergeByPropertyObjectTrumpMergePolicy. - Marking NSManagedObject as Sendable: Managed objects are queue-bound. Do not mark
@unchecked Sendable.
Review Checklist
-
NSPersistentContainercreated once and shared across the app - Context access strictly guarded by
perform(_:)orperformAndWait(_:) - No
NSManagedObjectinstances cross thread or context boundaries -
viewContext.automaticallyMergesChangesFromParentis enabled -
mergePolicyconfigured on contexts to handle conflicts cleanly - Batch operation results merged into active contexts via
mergeChanges -
NSFetchedResultsControllerrequests include explicit sort descriptors - In-memory test stores reuse a shared
NSManagedObjectModel
References
- Implementation patterns (Stack, FRC, Batch, Testing): references/core-data-patterns.md
- Cross-process tracking: references/persistent-history.md
- Staged migration: references/staged-migration.md
- Core Data
- NSPersistentContainer
- NSFetchedResultsController
- NSStagedMigrationManager