How do you use Core Data across threads: contexts, perform, merging changes and merge policies?
Predict first
A background context changes and saves a name. What does the already-loaded object in the main context show, before and after a refresh?
import CoreData
func makeModel(withNickname: Bool = false) -> NSManagedObjectModel {
let m = NSManagedObjectModel()
let e = NSEntityDescription()
e.name = "Person"
e.managedObjectClassName = "NSManagedObject"
let n = NSAttributeDescription()
n.name = "name"
n.attributeType = .stringAttributeType
n.isOptional = false
var props = [n]
if withNickname {
let k = NSAttributeDescription()
k.name = "nick"
k.attributeType = .stringAttributeType
k.isOptional = true
props.append(k)
}
e.properties = props
m.entities = [e]
return m
}
let container = NSPersistentContainer(name: "T", managedObjectModel: makeModel())
let desc = NSPersistentStoreDescription()
desc.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [desc]
container.loadPersistentStores { _, e in if let e { print("load error", e) } }
let ctx = container.viewContext
let p = NSEntityDescription.insertNewObject(forEntityName: "Person", into: ctx)
p.setValue("Asha", forKey: "name")
try ctx.save()
let bg = container.newBackgroundContext()
bg.performAndWait {
let o = try! bg.fetch(NSFetchRequest<NSManagedObject>(entityName: "Person"))[0]
o.setValue("Bina", forKey: "name")
try! bg.save()
}
print(p.value(forKey: "name") as! String)
ctx.refreshAllObjects()
print(p.value(forKey: "name") as! String, ctx.automaticallyMergesChangesFromParent)
- Bina, then Bina false
- Asha, then Bina false
- Asha, then Asha true
Mental model. Each context is a person with their own photocopy of the document. Edits in one copy do not appear in another until someone sends the changes across, and if two people edited the same line, a rule must say whose edit wins.
A managed object context is not thread-safe; it is bound to a queue. The rules: viewContext lives on the main queue and is for UI reads and small edits; do imports and heavy writes in a background context (container.newBackgroundContext() or performBackgroundTask), and always touch a context’s objects inside context.perform { } (or performAndWait, or the async perform), which runs your block on that context’s queue. Do not share managed objects between contexts; pass NSManagedObjectIDs (which are thread-safe) and refetch with context.object(with:) in the other one. In Swift 6 terms, contexts and objects are not Sendable, which is exactly why the id is the currency. A newBackgroundContext() sees the store’s committed data: a fetch in it returns what has been saved.
Changes do not propagate by themselves. When a background context saves, the main context’s already-loaded objects still show the old values until they are refreshed. To keep the UI in sync set viewContext.automaticallyMergesChangesFromParent = true (it is false by default on a plain context), which merges saved changes from other contexts, or observe NSManagedObjectContextDidSave and call mergeChanges(fromContextDidSave:), or call refreshAllObjects(); NSFetchedResultsController then reports the change. When two contexts change the same object a conflict occurs at save: the merge policy decides who wins. The default (NSErrorMergePolicy) throws; NSMergeByPropertyObjectTrumpMergePolicy keeps the in-memory object’s changed properties; NSMergeByPropertyStoreTrumpMergePolicy prefers the store’s values; NSOverwriteMergePolicy overwrites. Pick one explicitly for a context that imports. For bulk work use batch operations (NSBatchInsertRequest, NSBatchUpdateRequest, NSBatchDeleteRequest), which go straight to SQLite without loading objects (very fast) but bypass context events and validation, so you must merge their results (NSPersistentHistoryTracking and the merge of remote changes help). Save in reasonable batches and reset the context to release memory during big imports.
The main context keeps its cached values until refreshed or merged, and automatic merging is off by default.
Common traps
- Reading a context’s objects outside
perform. It is a data race that may crash intermittently; stay on the context’s queue. - Expecting the UI to see a background save automatically. Turn on
automaticallyMergesChangesFromParentor merge the save notification.
They might follow up with
- What do you pass between contexts?
NSManagedObjectID, thenobject(with:)in the other context. - Which merge policy keeps your in-memory edits on conflict?
NSMergeByPropertyObjectTrumpMergePolicy.
Related questions
- 13.3.1 What are the parts of the Core Data stack, what is a managed object context, and what is a fault?
- 13.4.2 How do ModelActor, context-per-actor and VersionedSchema work in SwiftData, and what can you pass between…
- 10.4.1 What does an actor guarantee, and what is actor reentrancy?
← What are the parts of the Core Data stack, what is a managed object… How do Core Data migrations work: lightweight, staged and… →