Core Data
01 / 02

Stack Setup, Fetching & Saving

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