Local Persistence: SwiftData vs. Core Data

iOS Development — Architecture & Data

Chapter 4 · Local Persistence: SwiftData vs. Core Data

TaskFlow's own tasks vanish every time the app relaunches — nothing was ever actually saved to disk. This chapter covers Apple's real, current answer for local persistence, SwiftData, honestly contrasted against the older framework it builds on, Core Data.

Core Data: Real, Established, Genuinely Complex

Core Data is Apple's long-established real object-graph and persistence framework — genuinely powerful, and genuinely more complex to set up than most of what this course has covered so far. A typical Core Data model requires a separate .xcdatamodeld editor file, NSManagedObject subclasses, and manual NSPersistentContainer setup before a single object can be saved.

A Real, Honest Trade-Off
Core Data isn't obsolete — it remains real, actively used in countless existing apps, and offers some advanced real capabilities (fine-grained migration control, CloudKit integration matured over many years) that SwiftData, being newer, is still catching up to in places. For a brand-new app's own basic persistence needs, though, the real complexity Core Data requires is rarely worth it anymore.

SwiftData: Apple's Real, Current Answer

Introduced at WWDC 2023 alongside iOS 17 and Swift 5.9 — the same real release that brought the Observation framework (Fundamentals Chapter 6) — SwiftData is Apple's modern, Swift-native persistence framework, built on Apple's own established Core Data persistence engine but exposed through a genuinely different, dramatically simpler real API.

@Model

A real macro turning a plain Swift class into a persisted model — no separate schema editor file needed.

@Query

A real property wrapper fetching and automatically observing persisted data directly inside a SwiftUI view.

ModelContainer

Manages the real schema and on-disk storage — set up once, typically at the app's own root.

ModelContext

Tracks in-memory changes and saves them — SwiftData's real equivalent of Core Data's NSManagedObjectContext.

Turning Task into a Real, Persisted Model

@Model final class Task { var title: String var isDone: Bool var priority: Int init(title: String, isDone: Bool = false, priority: Int = 1) { self.title = title self.isDone = isDone self.priority = priority } }
A Real, Genuinely Notable Change
Task is now a real class, not the struct Fundamentals used — SwiftData's own @Model macro requires a reference type, since persisted objects need a real, stable identity across fetches and edits, the same underlying reason NSManagedObject has always been a class in Core Data too. This is a genuine, deliberate exception to Fundamentals Chapter 4's own "prefer structs" guidance — persistence is exactly the kind of case that guidance already flagged as needing shared, mutable identity.

Wiring Up the ModelContainer

@main struct TaskFlowApp: App { var body: some Scene { WindowGroup { TaskListView() } } .modelContainer(for: Task.self) }

The real .modelContainer(for:) modifier sets up SwiftData's own on-disk storage for Task once, at the app's own root — every view inside WindowGroup can then reach a shared ModelContext automatically, without passing it down by hand.

Fetching with @Query

struct TaskListView: View { @Query private var tasks: [Task] @Environment(\.modelContext) private var modelContext var body: some View { List(tasks) { task in Text(task.title) } } func addTask(title: String) { let newTask = Task(title: title) modelContext.insert(newTask) } }
Real, Automatic Live Updates
@Query behaves like a real, persisted cousin of Fundamentals Chapter 6's own @State — a view using it redraws automatically whenever the underlying data changes, with no manual refresh call needed, whether that change came from this view's own modelContext.insert(...) or from anywhere else in the app.
Core DataSwiftData
Real Introduction2005 (macOS), long-established on iOSiOS 17, Swift 5.9, WWDC 2023
Model DefinitionA separate .xcdatamodeld editor fileA plain Swift class with @Model
FetchingNSFetchRequest, real, verbose setup@Query, a single property wrapper
Best Real FitExisting Core Data apps, advanced/legacy needsNew apps' own straightforward persistence

Hands-On Exercises

Exercise 1

Write a real @Model class JournalEntry with var text: String and var date: Date, plus a real initializer, and a view using @Query private var entries: [JournalEntry] to display all entries in a List.

📄 View solution
Exercise 2

Write a real function deleteTask(_ task: Task, context: ModelContext) using SwiftData's own context.delete(task) method, and explain, in your own words, why this call doesn't need an explicit "save" step for the deletion to persist to disk.

📄 View solution
Exercise 3

Explain, in your own words, why SwiftData's @Model requires a class rather than a struct, connecting your answer directly to Fundamentals Chapter 4's own value-type/reference-type distinction and its real "when to reach for a class instead" guidance.

📄 View solution

Chapter 4 Quick Reference

  • Core Data — real, established, genuinely more complex; still the right real fit for existing apps and some advanced needs
  • SwiftData (iOS 17/Swift 5.9, WWDC 2023) — built on Core Data's own engine, exposed through a dramatically simpler real API
  • @Model turns a plain class into a persisted model — no separate schema file
  • @Query fetches and automatically observes persisted data, like a persisted cousin of @State
  • ModelContainer (schema/storage) and ModelContext (in-memory changes/saving) are SwiftData's real core pieces
  • @Model classes are genuinely a deliberate exception to "prefer structs" — persistence needs real, stable, shared identity