Core Data vs SwiftData for persistence — how do you choose in a new app?
Core Data has a decade of battle-scars; SwiftData has a fraction of the code — choosing between them for a new app is a real architectural decision.
LEARN BY DOINGPut your intuition to work3 steps
Core Data vs SwiftData for persistence — how do you choose in a new app?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two frameworks, one underlying job
Both Core Data and SwiftData exist to persist Swift model objects to disk and query them back efficiently, with change tracking and (optionally) CloudKit sync. Core Data has shipped since iOS 3 in 2009; SwiftData arrived at WWDC 2023 for iOS 17+, built as a Swift-native layer over persistence infrastructure with deep lineage in Core Data. An interviewer asking you to choose between them wants a real decision framework, not a "SwiftData is newer so it's better" reflex answer.
#Core Data, in practice
A Core Data stack centers on an .xcdatamodeld file (a visual model editor) generating NSManagedObject subclasses, an NSPersistentContainer that owns the store and contexts, an NSManagedObjectContext you fetch and save through, and NSFetchRequest with NSPredicate/NSSortDescriptor for querying. NSFetchedResultsController drives UITableView/UICollectionView updates efficiently in UIKit. It supports lightweight automatic migrations for additive schema changes and fully custom mapping models for complex ones, and NSPersistentCloudKitContainer adds CloudKit sync with more control than SwiftData currently exposes. The cost is real boilerplate — a separate model file, generated classes, and an API surface rooted in Objective-C dynamism (NSManagedObject instances are "faulted" in and out of memory, which is powerful but occasionally surprising to debug).
#SwiftData, in practice
@Model turns a plain Swift class into a persisted entity directly in code — no separate model file, no generated subclass to keep in sync. @Query is a property wrapper that fetches straight into a SwiftUI view and keeps it updated reactively as the store changes, replacing NSFetchedResultsController and its delegate callbacks with a few lines. ModelContainer and ModelContext play the roles of NSPersistentContainer and NSManagedObjectContext. Migrations are expressed through VersionedSchema and a SchemaMigrationPlan. Under the hood, SwiftData is built on the same persistence lineage as Core Data — it isn't a from-scratch database engine — which is part of why the two can coexist or migrate between each other more realistically than switching to something unrelated.
#How to actually choose
Deployment target is the hard constraint: anything below iOS 17 rules SwiftData out entirely. Above that floor, a greenfield SwiftUI app benefits from SwiftData's much smaller boilerplate and native @Query reactivity — less code doing the same job for straightforward, entity-per-screen data. Reach for Core Data instead when you need capabilities SwiftData doesn't fully expose yet — fetched properties, fine-grained NSFetchedResultsController control for a UIKit list, complex custom migration mapping models, or store configurations Core Data has supported for over a decade. And if a large, working Core Data model already exists in production, that's a strong argument for leaving it alone rather than rewriting for its own sake — SwiftData's maturity, especially around complex migrations and predicate expressiveness, is still catching up as of iOS 17/18.
#The predicate gap
#Predicate in SwiftData is a compile-time-checked Swift closure, which is genuinely safer than NSPredicate's stringly-typed format strings — a typo in a key path is a compiler error instead of a runtime crash. But as of iOS 17/18 it supports a narrower set of expressions: some string operations, certain subqueries, and some collection operations that NSPredicate handles routinely still don't compile in #Predicate. Check the specific query patterns a feature needs before committing to SwiftData for data-heavy work.
#In code
// Core Data: entity defined in the .xcdatamodeld, generated NSManagedObject subclass.extension Task {@NSManaged var title: String@NSManaged var isDone: Bool@NSManaged var createdAt: Date}func fetchOpenTasks(context: NSManagedObjectContext) throws -> [Task] {let request = Task.fetchRequest()request.predicate = NSPredicate(format: "isDone == NO")request.sortDescriptors = [NSSortDescriptor(keyPath: \Task.createdAt, ascending: false)]return try context.fetch(request) as! [Task]}
// SwiftData: the model IS the Swift type, no separate data model file.import SwiftData@Modelfinal class Task {var title: Stringvar isDone: Boolvar createdAt: Dateinit(title: String, isDone: Bool = false, createdAt: Date = .now) {self.title = titleself.isDone = isDoneself.createdAt = createdAt}}struct TaskListView: View {@Query(filter: #Predicate<Task> { !$0.isDone },sort: \Task.createdAt, order: .reverse)private var openTasks: [Task]@Environment(\.modelContext) private var contextvar body: some View {List(openTasks) { task inText(task.title)}.toolbar {Button("Add") {context.insert(Task(title: "New task"))}}}}
#Deciding
🧠 Check your understanding
0/3 · 0/3 answered1. What is the main constraint that would force you to use Core Data instead of SwiftData in a new app?
2. In SwiftData, what fetches a live, reactive list of model objects directly into a SwiftUI view?
3. How does #Predicate in SwiftData differ from NSPredicate?