```html SwiftUI: How to Build Core Data Stack (iOS 17+, 2026)

How to Build a Core Data Stack in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: NSPersistentContainer Updated: May 12, 2026
TL;DR

Create a PersistenceController struct that owns an NSPersistentContainer, expose a shared singleton, then inject container.viewContext into the SwiftUI environment from your App entry point. That's the entire stack.

// PersistenceController.swift
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "MyModel")
        if inMemory {
            container.persistentStoreDescriptions.first!.url =
                URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores { _, error in
            if let error { fatalError("Core Data load failed: \(error)") }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

Full implementation

The pattern below gives you a PersistenceController with a preview static that uses an in-memory store (so Xcode Previews never touch the disk), a background save helper, and a sample entity called Item with a timestamp attribute. The App struct injects the view context once, and every child view reads it with @Environment(\.managedObjectContext).

// MARK: - PersistenceController.swift
import CoreData

struct PersistenceController {

    // MARK: Singletons
    static let shared = PersistenceController()

    /// In-memory store for Xcode Previews — no disk I/O, no state leakage.
    static let preview: PersistenceController = {
        let controller = PersistenceController(inMemory: true)
        let ctx = controller.container.viewContext
        // Seed sample data
        for i in 0.<5 {
            let item = Item(context: ctx)
            item.timestamp = Date().addingTimeInterval(Double(i) * -3600)
        }
        try? ctx.save()
        return controller
    }()

    // MARK: Container
    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "MyModel") // must match .xcdatamodeld filename

        if inMemory {
            container.persistentStoreDescriptions.first!.url =
                URL(fileURLWithPath: "/dev/null")
        }

        container.loadPersistentStores { description, error in
            if let error {
                // Replace with your crash reporter in production
                fatalError("Core Data store failed to load: \(error.localizedDescription)")
            }
        }

        // Merge remote / background changes into the view context automatically
        container.viewContext.automaticallyMergesChangesFromParent = true
        // Prefer in-memory data when a merge conflict arises
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }

    // MARK: Background save helper
    /// Perform heavy writes on a private-queue context to keep the UI responsive.
    func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
        container.performBackgroundTask { ctx in
            ctx.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
            block(ctx)
            guard ctx.hasChanges else { return }
            try? ctx.save()
        }
    }
}

// MARK: - MyApp.swift
import SwiftUI

@main
struct MyApp: App {
    let persistence = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext,
                              persistence.container.viewContext)
        }
    }
}

// MARK: - ContentView.swift
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [SortDescriptor(\.timestamp, order: .reverse)],
        animation: .default
    )
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    Text(item.timestamp?.formatted() ?? "Unknown")
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Add", systemImage: "plus", action: addItem)
                }
            }
            .navigationTitle("Items")
        }
    }

    private func addItem() {
        withAnimation {
            let item = Item(context: viewContext)
            item.timestamp = Date()
            saveContext()
        }
    }

    private func deleteItems(at offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)
            saveContext()
        }
    }

    private func saveContext() {
        do {
            try viewContext.save()
        } catch {
            print("Save error: \(error.localizedDescription)")
        }
    }
}

#Preview {
    ContentView()
        .environment(\.managedObjectContext,
                      PersistenceController.preview.container.viewContext)
}

How it works

  1. NSPersistentContainer(name:) — The string "MyModel" must exactly match your .xcdatamodeld bundle filename. The container locates the model file automatically from the main bundle and sets up both the managed object model and the persistent store coordinator for you.
  2. loadPersistentStores — This call is synchronous on the calling thread and opens (or creates) the SQLite file on disk. The inMemory path swaps the URL to /dev/null so the preview container never writes to the simulator's sandbox.
  3. automaticallyMergesChangesFromParent — When performBackgroundTask saves on a private-queue context, Core Data posts a NSManagedObjectContextDidSave notification. Setting this flag to true makes the view context absorb those changes automatically, keeping @FetchRequest results up to date with zero manual merging.
  4. .environment(\.managedObjectContext, …) — Injecting the view context once at the App level propagates it down the entire view hierarchy, so any child can declare @Environment(\.managedObjectContext) without prop drilling.
  5. @FetchRequest — Declared with a SortDescriptor using the Swift 5.7+ key-path syntax. SwiftUI re-renders ContentView automatically whenever the underlying fetch results change, behaving like a reactive database query.

Variants

CloudKit-backed stack (NSPersistentCloudKitContainer)

Swap one type to get automatic iCloud sync — no extra networking code required. Add the CloudKit and Remote Notifications capabilities in Xcode first.

import CoreData
import CloudKit

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer // ← only change

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "MyModel")

        if inMemory {
            container.persistentStoreDescriptions.first!.url =
                URL(fileURLWithPath: "/dev/null")
        } else {
            // Enable remote change notifications so the view context
            // updates when another device pushes data.
            let description = container.persistentStoreDescriptions.first!
            description.setOption(true as NSNumber,
                forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        }

        container.loadPersistentStores { _, error in
            if let error { fatalError(error.localizedDescription) }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}

Predicated fetch with dynamic filtering

Use @FetchRequest(fetchRequest:) or the fetchRequest.predicate property to filter at the database level — far more efficient than filtering a Swift array post-fetch for large data sets. Example: NSPredicate(format: "timestamp > %@", startDate as CVarArg). Assign the predicate to the @FetchRequest's nsPredicate property inside an .onChange(of:) modifier to update the query reactively as the user types a search term.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a Core Data stack in SwiftUI for iOS 17+.
Use NSPersistentContainer with a shared singleton and
an in-memory preview instance.
Inject container.viewContext via .environment on the App entry point.
Add a performBackgroundTask helper for off-main-thread writes.
Make it accessible (VoiceOver labels on List rows).
Add a #Preview with realistic sample data (at least 5 seeded Item objects).

In the Soarias Build phase, drop this prompt into an Implementation task so Claude Code scaffolds the full persistence layer — including the .xcdatamodeld — before you wire up any feature screens.

Related

FAQ

Does this work on iOS 16?

Yes — NSPersistentContainer has been available since iOS 10. The code above uses the SortDescriptor key-path syntax and the #Preview macro, both of which require iOS 16+ and Xcode 15+ respectively. For iOS 15 targets, replace SortDescriptor(\.timestamp) with NSSortDescriptor(keyPath: \Item.timestamp, ascending: false) and replace #Preview with a PreviewProvider.

Should I use Core Data or SwiftData in a new iOS 17+ app?

SwiftData (introduced in iOS 17) is the modern replacement and requires significantly less boilerplate — just @Model classes and a ModelContainer. Core Data is the right choice if you need fine-grained migration control, have an existing Core Data model, require NSFetchedResultsController, or are targeting iOS 16 and below. Both stacks can coexist; SwiftData can sit on top of a Core Data persistent store.

What's the UIKit equivalent?

In UIKit, the NSPersistentContainer setup is identical. The difference is context propagation: instead of .environment(\.managedObjectContext, …), you pass the context manually to each UIViewController (dependency injection), or retrieve it via the shared singleton. NSFetchedResultsController plays the role of @FetchRequest, driving UITableView / UICollectionView updates through its delegate.

Last reviewed: 2026-05-12 by the Soarias team.

```