Core Data
Core Data persistence framework for modeling, storing, and querying structured data in iOS apps
You are an expert in Core Data for building iOS applications with Swift. ## Key Points 1. **`NSManagedObjectModel`** — Schema definition (entities, attributes, relationships). 2. **`NSPersistentStoreCoordinator`** — Mediates between the model and the store file. 3. **`NSManagedObjectContext`** — In-memory scratchpad where you create, fetch, update, and delete objects. - **Save judiciously.** Every `save()` hits the persistent store. Batch related changes into a single save rather than saving after each modification. - **Use `NSFetchedResultsController` (UIKit) or `@FetchRequest` (SwiftUI)** for live queries instead of manually re-fetching. - **Set a `mergePolicy`** on every context. The default policy throws on conflicts, which is rarely desired. `NSMergeByPropertyObjectTrumpMergePolicy` (in-memory wins) is a common choice. - **Keep your model lean.** Store large blobs (images, files) on disk and keep only file paths or small thumbnails in Core Data. Use "Allows External Storage" for binary attributes. - **Use `fetchBatchSize`** on fetch requests to limit memory usage when loading many objects. A value of 20 is a reasonable default. - **Threading violations.** `NSManagedObject` instances are not thread-safe. Never pass an object between threads — pass its `objectID` and re-fetch in the target context. - **Faulting surprises.** Core Data objects are faults (lazy proxies) by default. Accessing a property triggers a fetch. Batch-faulting with `relationshipKeyPathsForPrefetching` avoids N+1 queries. - **Forgetting to save.** Changes exist only in memory until `save()` is called. If the app crashes before saving, data is lost. - **Cascade delete surprises.** If a relationship has a cascade delete rule, deleting a parent removes all children. Verify delete rules match your intent. ## Quick Example ```swift let description = NSPersistentStoreDescription() description.shouldMigrateStoreAutomatically = true description.shouldInferMappingModelAutomatically = true container.persistentStoreDescriptions = [description] ```
skilldb get ios-swift-skills/Core DataFull skill: 280 linesCore Data — iOS/Swift
You are an expert in Core Data for building iOS applications with Swift.
Core Philosophy
Core Data is not a database -- it is an object graph manager that happens to persist to a store. This distinction matters because it shapes how you should think about every interaction with it. You are not writing SQL queries against rows in tables; you are managing a graph of interconnected objects in memory, and Core Data handles the persistence transparently. When you internalize this, decisions about fetching, faulting, contexts, and relationships become intuitive rather than surprising.
Threading discipline is non-negotiable with Core Data. Every managed object is bound to the context that created it, and contexts are bound to specific queues. Passing a managed object from a background context to the main thread is not just bad practice -- it is undefined behavior that will corrupt data or crash your app. The safe pattern is always the same: pass the objectID, fetch the object on the target context. This strictness is the price of Core Data's power; respecting it eliminates an entire category of concurrency bugs.
Performance in Core Data comes from understanding faulting and batch sizes. Core Data objects start as faults -- lightweight proxies that fetch their data lazily on first property access. This is a feature, not a bug, because it means you can hold references to thousands of objects without consuming memory for all of them. But it also means that iterating over a large collection and accessing a property on each object triggers a separate fetch per object (the N+1 problem). Setting fetchBatchSize and using relationshipKeyPathsForPrefetching turns these N+1 fetches into efficient batched loads.
Anti-Patterns
-
Performing writes on the view context: The view context is meant for reads that drive the UI. Writing to it blocks the main thread during the save, and if the write fails, your UI is left in an inconsistent state. Always create a background context for writes, and let
automaticallyMergesChangesFromParentpropagate the changes to the view context. -
Passing managed objects across threads: Handing an
NSManagedObjectfrom a background context to a view running on the main thread causes data corruption or crashes. Pass theobjectIDand re-fetch in the target context. This rule has no exceptions. -
Saving after every single change: Calling
context.save()after modifying each individual object means each save hits the persistent store and triggers change notifications. Batch related changes into a single save operation to reduce I/O and keep UI updates efficient. -
Storing large binary data directly in Core Data: Putting multi-megabyte images or files as binary attributes bloats the SQLite store, slows fetches, and increases memory pressure. Store large blobs on the filesystem and keep only file paths or small thumbnails in Core Data. Use the "Allows External Storage" option for binary attributes as a compromise.
-
Ignoring merge policies and getting unexpected crashes: The default merge policy throws an error on conflicts, which will crash your app if two contexts modify the same object simultaneously. Set an explicit merge policy like
NSMergeByPropertyObjectTrumpMergePolicyon every context to handle conflicts gracefully.
Overview
Core Data is Apple's object-graph and persistence framework. It manages the lifecycle of model objects, provides an in-memory object graph with change tracking, supports undo/redo, and persists data to SQLite (the most common backing store), XML, or binary formats. With NSPersistentCloudKitContainer, it also syncs data across devices via iCloud. Core Data is not a database — it is an object-graph manager that happens to persist to a store.
Core Concepts
The Core Data Stack
The stack consists of three layers:
NSManagedObjectModel— Schema definition (entities, attributes, relationships).NSPersistentStoreCoordinator— Mediates between the model and the store file.NSManagedObjectContext— In-memory scratchpad where you create, fetch, update, and delete objects.
NSPersistentContainer bundles all three:
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "AppModel")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { description, error in
if let error {
fatalError("Core Data store failed to load: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
var viewContext: NSManagedObjectContext {
container.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
container.newBackgroundContext()
}
}
Defining Entities with NSManagedObject
Define entities in the .xcdatamodeld editor, then generate or manually write NSManagedObject subclasses:
// Auto-generated or manual NSManagedObject subclass
@objc(Task)
public class Task: NSManagedObject {
@NSManaged public var id: UUID
@NSManaged public var title: String
@NSManaged public var isCompleted: Bool
@NSManaged public var createdAt: Date
@NSManaged public var category: Category? // to-one relationship
}
extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
NSFetchRequest<Task>(entityName: "Task")
}
static func create(in context: NSManagedObjectContext, title: String) -> Task {
let task = Task(context: context)
task.id = UUID()
task.title = title
task.isCompleted = false
task.createdAt = Date()
return task
}
}
CRUD Operations
// CREATE
let task = Task.create(in: viewContext, title: "Buy groceries")
try viewContext.save()
// READ
let request = Task.fetchRequest()
request.predicate = NSPredicate(format: "isCompleted == %@", NSNumber(value: false))
request.sortDescriptors = [NSSortDescriptor(keyPath: \Task.createdAt, ascending: false)]
request.fetchLimit = 50
let tasks = try viewContext.fetch(request)
// UPDATE
task.isCompleted = true
try viewContext.save()
// DELETE
viewContext.delete(task)
try viewContext.save()
FetchRequest in SwiftUI
@FetchRequest provides live-updating results directly in views:
struct TaskListView: View {
@Environment(\.managedObjectContext) private var context
@FetchRequest(
sortDescriptors: [SortDescriptor(\.createdAt, order: .reverse)],
predicate: NSPredicate(format: "isCompleted == NO"),
animation: .default
)
private var tasks: FetchedResults<Task>
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.onDelete(perform: deleteTasks)
}
}
private func deleteTasks(at offsets: IndexSet) {
offsets.map { tasks[$0] }.forEach(context.delete)
try? context.save()
}
}
Dynamic predicates with @SectionedFetchRequest:
struct CategorizedTasksView: View {
@SectionedFetchRequest(
sectionIdentifier: \.category?.name,
sortDescriptors: [
SortDescriptor(\.category?.name),
SortDescriptor(\.createdAt, order: .reverse)
]
)
private var sections: SectionedFetchResults<String?, Task>
var body: some View {
List {
ForEach(sections) { section in
Section(section.id ?? "Uncategorized") {
ForEach(section) { task in
TaskRow(task: task)
}
}
}
}
}
}
Implementation Patterns
Background Context for Heavy Operations
Never perform batch or long-running operations on the view context:
func importTasks(from records: [TaskRecord]) async throws {
let context = PersistenceController.shared.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try await context.perform {
for record in records {
let task = Task(context: context)
task.id = record.id
task.title = record.title
task.isCompleted = record.isCompleted
task.createdAt = record.createdAt
}
try context.save()
}
// viewContext picks up changes via automaticallyMergesChangesFromParent
}
Batch Operations
For large data sets, batch operations bypass the object graph for performance:
func markAllCompleted() async throws {
let context = PersistenceController.shared.newBackgroundContext()
try await context.perform {
let batchUpdate = NSBatchUpdateRequest(entityName: "Task")
batchUpdate.propertiesToUpdate = ["isCompleted": true]
batchUpdate.predicate = NSPredicate(format: "isCompleted == NO")
batchUpdate.resultType = .updatedObjectIDsResultType
let result = try context.execute(batchUpdate) as? NSBatchUpdateResult
let objectIDs = result?.result as? [NSManagedObjectID] ?? []
// Merge changes into viewContext so UI updates
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSUpdatedObjectsKey: objectIDs],
into: [PersistenceController.shared.viewContext]
)
}
}
Lightweight Migration
Core Data supports automatic lightweight migration for simple schema changes (adding attributes, making optional, renaming with renaming ID):
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
container.persistentStoreDescriptions = [description]
CloudKit Sync
let container = NSPersistentCloudKitContainer(name: "AppModel")
let description = container.persistentStoreDescriptions.first!
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.example.app"
)
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
Best Practices
- Use background contexts for writes and batch operations. The view context should be reserved for reads that drive the UI. Set
automaticallyMergesChangesFromParent = trueon the view context so it picks up background saves. - Save judiciously. Every
save()hits the persistent store. Batch related changes into a single save rather than saving after each modification. - Use
NSFetchedResultsController(UIKit) or@FetchRequest(SwiftUI) for live queries instead of manually re-fetching. - Set a
mergePolicyon every context. The default policy throws on conflicts, which is rarely desired.NSMergeByPropertyObjectTrumpMergePolicy(in-memory wins) is a common choice. - Keep your model lean. Store large blobs (images, files) on disk and keep only file paths or small thumbnails in Core Data. Use "Allows External Storage" for binary attributes.
- Use
fetchBatchSizeon fetch requests to limit memory usage when loading many objects. A value of 20 is a reasonable default.
Common Pitfalls
- Threading violations.
NSManagedObjectinstances are not thread-safe. Never pass an object between threads — pass itsobjectIDand re-fetch in the target context. - Faulting surprises. Core Data objects are faults (lazy proxies) by default. Accessing a property triggers a fetch. Batch-faulting with
relationshipKeyPathsForPrefetchingavoids N+1 queries. - Forgetting to save. Changes exist only in memory until
save()is called. If the app crashes before saving, data is lost. - Retain cycles with fetched objects. Long-lived contexts holding large object graphs consume memory. Use
context.reset()orrefreshAllObjects()periodically in long-running background contexts. - Cascade delete surprises. If a relationship has a cascade delete rule, deleting a parent removes all children. Verify delete rules match your intent.
Install this skill directly: skilldb add ios-swift-skills
Related Skills
App Architecture
MVVM and Clean Architecture patterns for structuring scalable, testable iOS applications
Combine
Combine framework reactive programming patterns for handling asynchronous events in iOS
Navigation
SwiftUI navigation patterns using NavigationStack, programmatic routing, and deep linking
Networking
URLSession networking patterns for building robust API clients and handling data transfer in iOS
Swift Concurrency
Swift structured concurrency with async/await, actors, and task groups for safe concurrent iOS code
Swiftui
SwiftUI declarative UI framework fundamentals for building modern iOS interfaces