SwiftData Basics
SwiftData is Apple's modern persistence framework, introduced to give developers a Swift-native way to define and store model data without writing a Core Data schema by hand. Instead of .xcdatamodeld files and NSManagedObject subclasses, you annotate a plain Swift class with the @Model macro, and SwiftData generates the storage, change tracking, and relationship management for you at compile time. Under the hood SwiftData still runs on top of Core Data's proven storage engine, so it inherits battle-tested behavior around migrations, faulting, and undo, while exposing a far simpler API surface that integrates directly with SwiftUI's @Query and ModelContext.
Cricket analogy: Instead of manually maintaining a paper scorebook ledger by hand like a Core Data schema, a modern scoring app auto-generates the full record from a simple team sheet (@Model), still built on the same trusted scoring rules underneath but far easier to use.
Defining a Model
A SwiftData model is a class marked with @Model. The macro rewrites stored properties to be persisted automatically — you do not need @Published or manual Core Data attribute wiring. Properties can be simple value types (String, Int, Date, Bool), arrays of simple types, or relationships to other @Model classes. Optional properties become optional columns; non-optional properties are required. You can also mark a property @Attribute(.unique) to enforce uniqueness, or exclude a computed property from persistence entirely since only stored properties are persisted by default.
Cricket analogy: A player's profile card automatically saves their name, team, and batting average without anyone manually filing them; their jersey number must be unique across the squad (@Attribute(.unique)), while their form rating computed from recent scores isn't stored, just recalculated on demand.
import SwiftData
@Model
final class Task {
var title: String
var isCompleted: Bool
var createdAt: Date
var priority: Int
@Relationship(deleteRule: .cascade)
var subtasks: [Subtask] = []
init(title: String, priority: Int = 0) {
self.title = title
self.isCompleted = false
self.createdAt = .now
self.priority = priority
}
}
@Model
final class Subtask {
var title: String
var isCompleted: Bool = false
init(title: String) {
self.title = title
}
}Setting Up the Model Container
To use SwiftData in a SwiftUI app, you attach a ModelContainer to your WindowGroup using the .modelContainer(for:) modifier. This creates (or opens) the underlying persistent store and injects a ModelContext into the environment that every descendant view can access. The container is the schema-level object — it owns the store file and configuration — while the context is a lightweight, per-scope workspace where you insert, delete, and save changes. Views typically don't touch the container directly; they read the context via @Environment(\.modelContext) and query data via the @Query property wrapper.
Cricket analogy: The stadium's master archive (ModelContainer) is set up once when the ground opens for the season, owning the full historical record, while each match-day scorer (ModelContext) gets a lightweight working copy to log today's overs without touching the whole archive directly.
@main
struct TaskApp: App {
var body: some Scene {
WindowGroup {
TaskListView()
}
.modelContainer(for: Task.self)
}
}
struct TaskListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
var body: some View {
List {
ForEach(tasks) { task in
Text(task.title)
}
.onDelete { indexSet in
for index in indexSet {
modelContext.delete(tasks[index])
}
}
}
}
}Querying and Filtering
@Query supports sort descriptors and predicates written with the type-safe #Predicate macro, which compiles your Swift closure into an executable query against the store rather than fetching everything into memory and filtering in Swift. This is important for performance on large datasets — a #Predicate runs at the storage layer, while filtering an already-fetched array does not. You can also construct FetchDescriptor values manually and call modelContext.fetch(_:) when you need dynamic queries built at runtime rather than compile time, such as search-as-you-type filtering.
Cricket analogy: Asking the ground's archive system to return only centuries scored at Lord's (#Predicate) runs the filter at the archive itself, far faster than pulling every innings ever played into the dugout and sorting by hand; a live search box during a broadcast needs a dynamic FetchDescriptor built on the fly.
SwiftData and Core Data can coexist in the same app because SwiftData's store format is compatible with Core Data's SQLite store. This makes it realistic to migrate an existing Core Data app to SwiftData incrementally rather than as a big-bang rewrite.
Inserting an object with modelContext.insert(_:) does not immediately write to disk. SwiftData autosaves on a schedule and around app lifecycle events, but if you need a guaranteed durable write (e.g., before showing a success message), call try modelContext.save() explicitly and handle the thrown error.
@Modelturns a plain Swift class into a persisted entity backed by Core Data's storage engine.ModelContainerowns the store;ModelContext(from the environment) is where you insert, delete, and save.@Queryfetches and observes model data reactively inside SwiftUI views, with optional sort and#Predicatefiltering.#Predicatecompiles to a store-level query, which is far more efficient than filtering an in-memory array.@Relationship(deleteRule:)controls cascade behavior when a parent object is deleted.- Explicit
try modelContext.save()is needed when you require a guaranteed synchronous write.
Practice what you learned
1. What macro converts a plain Swift class into a SwiftData persisted entity?
2. Which object should a SwiftUI view use to insert or delete SwiftData objects?
3. Why is `#Predicate` generally preferable to filtering an already-fetched Swift array?
4. What underlying technology does SwiftData's persistent store use?
5. What does `@Relationship(deleteRule: .cascade)` do?
Was this page helpful?
You May Also Like
SwiftUI Previews and UI Testing
Understand how Xcode Previews accelerate SwiftUI development and how XCUITest complements ViewModel unit tests by verifying real UI behavior.
UserDefaults and Persistence
Learn when and how to use UserDefaults for lightweight app settings and small persisted values, and how it compares to other persistence options.
Codable and JSON Parsing
Learn how Swift's Codable protocol pair enables automatic, type-safe encoding and decoding between Swift types and formats like JSON.
List and ForEach
Learn how List renders scrollable, platform-styled collections and how ForEach generates repeated views from a data collection using stable identity.