Core Data: Stack Setup, Fetching & Saving
Core Data is an object-graph and persistence framework, not itself a database — it manages NSManagedObject lifecycles, relationships, and change tracking on top of a store (SQLite by default, or in-memory for tests/previews).
Setting Up the Stack
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
// "MyApp" must match the .xcdatamodeld filename
container = NSPersistentContainer(name: "MyApp")
if inMemory {
// Useful for unit tests and SwiftUI previews — no disk I/O
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { description, error in
if let error {
// A real app should handle this gracefully (corrupted store,
// failed migration) rather than crash — but during development
// fatalError surfaces problems immediately
fatalError("Core Data failed to load: \(error)")
}
}
// Automatically merge changes saved on background contexts into viewContext
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
// SwiftUI App entry point
@main
struct MyApp: App {
let persistence = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistence.container.viewContext)
}
}
}Fetching
// Manual fetch request (Xcode's codegen creates Entity.fetchRequest())
let request: NSFetchRequest<Note> = Note.fetchRequest()
request.predicate = NSPredicate(format: "isPinned == YES AND title CONTAINS[cd] %@", searchText)
request.sortDescriptors = [NSSortDescriptor(keyPath: \Note.createdAt, ascending: false)]
request.fetchLimit = 50
request.fetchBatchSize = 20 // page results in from the store as needed
do {
let notes = try viewContext.fetch(request)
} catch {
print("Fetch failed: \(error)")
}
// SwiftUI: @FetchRequest keeps a view automatically in sync with matching data
struct NoteListView: View {
@FetchRequest(
sortDescriptors: [SortDescriptor(\Note.createdAt, order: .reverse)],
predicate: NSPredicate(format: "isPinned == YES")
) private var pinnedNotes: FetchedResults<Note>
var body: some View {
List(pinnedNotes) { note in
Text(note.title ?? "")
}
}
}Creating, Updating & Saving
// Create
let note = Note(context: viewContext)
note.id = UUID()
note.title = "New Note"
note.createdAt = Date()
// Update — mutating a fetched object only changes the in-memory context
note.title = "Updated title"
viewContext.hasChanges // true
// Nothing is persisted until save() is called
do {
try viewContext.save()
} catch {
// Common causes: validation failure (required attribute left nil,
// a constraint violated), or an unresolved merge conflict
print("Save failed: \(error)")
}
// Delete
viewContext.delete(note)
try? viewContext.save()
// Batch delete — executes at the store level, doesn't load objects into
// memory first. Must manually propagate the result into any live contexts.
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
deleteRequest.resultType = .resultTypeObjectIDs
let result = try viewContext.execute(deleteRequest) as? NSBatchDeleteResult
let changes = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [viewContext])Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free