Core Data
02 / 02

Contexts, Concurrency & Migrations

Core Data: Contexts, Concurrency & Migrations

Thread/Queue Confinement

// Managed objects are confined to the queue of the context that created
// them — passing one directly across threads is undefined behavior.

// WRONG — using viewContext's Note from a background thread
DispatchQueue.global().async {
    note.title = "Danger" // crash risk / data corruption
}

// RIGHT — do background work on its own context
container.performBackgroundTask { backgroundContext in
    let request: NSFetchRequest<Note> = Note.fetchRequest()
    let notes = try? backgroundContext.fetch(request)
    notes?.forEach { $0.isPinned = false }
    try? backgroundContext.save() // merges into viewContext automatically
                                    // if automaticallyMergesChangesFromParent = true
}

// Pass a reference to an object across contexts via its NSManagedObjectID,
// never the NSManagedObject instance itself
let objectID = note.objectID
container.performBackgroundTask { context in
    guard let noteInThisContext = try? context.existingObject(with: objectID) as? Note else { return }
    noteInThisContext.title = "Edited on background context"
    try? context.save()
}

Child Contexts & Merge Policies

// Child context — for a scratch/draft edit UI. Saving the child pushes
// changes up to its parent (in memory); the parent still needs its own
// save() to actually reach the store. Great for a Cancel button: just
// discard the child context without ever saving it.
let editContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
editContext.parent = viewContext

let noteToEdit = editContext.object(with: note.objectID) as! Note
noteToEdit.title = "Draft title"

// Commit: push up to parent, then save the parent to reach the store
try? editContext.save()
try? viewContext.save()

// Merge policy — how to resolve a conflict if the store changed since fetch
viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // in-memory wins per-property
// NSMergeByPropertyStoreTrumpMergePolicy — store wins per-property
// NSOverwriteMergePolicy — in-memory object wins entirely
// NSRollbackMergePolicy — store wins entirely, in-memory changes discarded

Migrations & Schema Changes

  • Add a new model version via Editor → Add Model Version in the .xcdatamodeld, then set it as the current version in the File Inspector.

  • Lightweight migration (Core Data infers the mapping automatically) handles simple changes: new attributes, removed attributes, some renames with a 'Renaming ID' hint.

  • NSPersistentContainer enables lightweight migration by default via NSMigratePersistentStoresAutomaticallyOption + NSInferMappingModelAutomaticallyOption.

  • Complex changes (splitting an entity, transforming data during migration) require a custom NSMappingModel and NSEntityMigrationPolicy subclass.

  • Always test migrations against a copy of a real, populated store from the previous version — an empty test store hides bugs that only appear with existing data.

CloudKit Sync

// NSPersistentCloudKitContainer syncs a store to the user's private
// iCloud database with minimal extra code — swap the container type
// and enable remote-change notifications.
let container = NSPersistentCloudKitContainer(name: "MyApp")
container.persistentStoreDescriptions.first?.setOption(
    true as NSNumber, forKey: NSPersistentHistoryTrackingKey
)
container.persistentStoreDescriptions.first?.setOption(
    true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey
)

// CloudKit-compatible schemas have constraints: every relationship must be
// optional, no unique constraints on most store types, no undoable
// attributes with certain configurations — check these before adopting it
// on an existing model.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free