How to Build a Core Data Stack in SwiftUI
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
-
NSPersistentContainer(name:) — The string
"MyModel"must exactly match your.xcdatamodeldbundle 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. -
loadPersistentStores — This call is synchronous on the calling thread and opens (or creates) the SQLite file on disk. The
inMemorypath swaps the URL to/dev/nullso the preview container never writes to the simulator's sandbox. -
automaticallyMergesChangesFromParent — When
performBackgroundTasksaves on a private-queue context, Core Data posts aNSManagedObjectContextDidSavenotification. Setting this flag totruemakes the view context absorb those changes automatically, keeping@FetchRequestresults up to date with zero manual merging. -
.environment(\.managedObjectContext, …) — Injecting the view context once at the
Applevel propagates it down the entire view hierarchy, so any child can declare@Environment(\.managedObjectContext)without prop drilling. -
@FetchRequest — Declared with a
SortDescriptorusing the Swift 5.7+ key-path syntax. SwiftUI re-rendersContentViewautomatically 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
-
⚠️ Mismatched model name — If the string passed to
NSPersistentContainer(name:)doesn't exactly match the.xcdatamodeldfilename (case-sensitive), the app crashes at launch with aCould not find NSManagedObjectModelerror. Always copy-paste the filename. -
⚠️ Saving on the wrong context — Core Data contexts are not thread-safe. Never call
viewContext.save()from a background thread. Usecontainer.performBackgroundTaskfor writes that happen off the main queue, and letautomaticallyMergesChangesFromParentpropagate the result to the view context. -
⚠️ @FetchRequest in non-injected previews — If a preview doesn't supply a
managedObjectContext, any@FetchRequestin the previewed view will crash. Always passPersistenceController.preview.container.viewContextin every#Previewmacro that renders a view with fetch requests. -
⚠️ Migration not configured for model changes — Adding or renaming an attribute after the app has shipped requires a new model version and a lightweight migration policy. Without it, the persistent store will fail to open on devices that already have data. Enable
NSMigratePersistentStoresAutomaticallyOptionandNSInferMappingModelAutomaticallyOptionon the store description.
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.