One. Course Details
This is the thirteenth lecture of Stanford University's CS193p iOS Application Development course for Spring 2025, taught by Paul Hegarty. The lecture begins with a conceptual introduction to SwiftData, Apple's modern persistence framework, followed by a hands-on demo converting the existing CodeBreaker game model to use SwiftData.
SwiftData stores data in a SQL database under the hood but provides a native Swift interface using standard types, structures, and closures. The lecture covers core SwiftData concepts including the @Model macro, supported data types, relationships, schema attributes, data operations, and querying. The demo focuses on modifying the model layer to support persistence, with querying, sorting, and searching functionality scheduled for the next lecture.
Two. Key Learning Takeaways
@Model marks classes for persistence and automatically makes them @Observable (never use both together). It only works on classes, not structs.
-
Supported property types include primitives (numbers, strings, booleans, dates, Data), other
@Modeltypes,Codabletypes, and arrays of any supported type. -
Arrays of
@Modelobjects are unordered by default, as they represent database joins. -
Use computed properties to wrap unsupported types or enforce ordering.
-
@Attribute(.unique)enforces unique values for a property. -
@Attribute(.externalStorage)stores large data (images, video) in the file system instead of the database. -
@Transientmarks properties that should not be persisted (only exist in memory). -
@Relationship(deleteRule: .cascade)automatically deletes related objects when the parent is deleted.
modelContext:
-
Use
modelContext.insert(_:)to add objects to the database. -
Use
modelContext.delete(_:)to remove objects from the database. -
Changes are automatically saved by default, with support for explicit saves and undo.
FetchDescriptor with three components:
-
The type of object to fetch.
-
A
#Predicateclosure that filters results (converted to SQL under the hood). -
An array of
SortDescriptors that define the result order.
@Query is a view macro that automatically fetches and updates results, triggering SwiftUI view updates when the database changes.
-
Static queries use fixed predicates and sort orders.
-
Dynamic queries require constructing the
@Queryin a view's initializer.
try/do-catch for throwing functions:
-
tryrequires a surroundingdo-catchblock. -
try!crashes on failure (use only during development). -
try?returns an optional (nil on failure).
Three. Course Gold Quotes
"SwiftData lets us interact much more at the Swift level where we're using normal structures and closures and things like that." "An@Model is automatically @Observable. So you never would put both @Model and @Observable. It makes no sense." "Each @Model is kind of going to end up being a table in a database." "Codable is great in that you can put pretty much anything in there, but it's also a little bit limited because when it's in the database, it's just a little blob." "Never have two pieces of state that represent the same thing. It always leads to bugs." "Destructive actions should look destructive. Use role: .destructive to make them stand out." "Your Model should be UI-independent. It should know nothing about Colors or SwiftUI." "Convenience initializers let you add UI-specific initialization without polluting your core Model."
Four. Layered Learning Notes
Module 1: SwiftData Core Concepts
SwiftData is Apple's modern replacement for Core Data, designed to integrate seamlessly with SwiftUI and the Swift language. It provides type-safe, declarative data persistence with minimal boilerplate code.The fundamental building block of SwiftData is the
@Model macro. Applying @Model to a class:
-
Generates all the code necessary to store and retrieve instances from the database
-
Automatically conforms the class to
Observable,Identifiable,Hashable, andEquatable -
Restricts property types to those that can be stored in a SQL database
Module 2: Model Definition and Schema
When defining your model classes, you can use several schema attributes to customize persistence behavior:
|
Attribute |
Purpose |
|---|---|
@Attribute(.unique) |
Ensures no two objects have the same value for this property |
@Attribute(.externalStorage) |
Stores large binary data in separate files |
@Transient |
Excludes the property from persistence |
@Relationship(deleteRule: .cascade) |
Deletes related objects when the parent is deleted |
@Relationship(inverse:) |
Defines the inverse side of a two-way relationship |
Supported property types:
-
All primitive Swift types (Int, String, Bool, Date, Data, etc.)
-
Other
@Modelclasses (creates database relationships) -
Types conforming to
Codable(stored as binary blobs) -
Arrays of any supported type
-
Arrays of
@Modelobjects are unordered. Use a sort descriptor or computed property to enforce order. -
Computed properties cannot be used in predicates, as they do not exist in the database.
-
Structs cannot be marked
@Model. Convert structs to classes or make themCodable.
Module 3: Data Operations
All interactions with the database go through aModelContext instance, which you access from the environment:swift
@Environment(\.modelContext) private var modelContextBasic operations:
-
Insert:
modelContext.insert(newGame)adds a new object to the database -
Delete:
modelContext.delete(game)removes an object from the database -
Save:
try modelContext.save()explicitly saves changes (autosave is enabled by default)
ModelContainer manages the database configuration, including:
-
Where the database is stored (on disk or in memory)
-
Schema migration rules for app updates
-
Which model classes are included in the schema
swift
@main
struct CodeBreakerApp: App {
var body: some Scene {
WindowGroup {
GameChooser()
}
.modelContainer(for: CodeBreaker.self)
}
}
Module 4: Querying Data
There are two primary ways to fetch data from the database:-
Manual fetching with
FetchDescriptor:
swiftlet descriptor = FetchDescriptor<CodeBreaker>( predicate: #Predicate { $0.name.contains("Mastermind") }, sortBy: [SortDescriptor(\.name, order: .forward)] ) do { let games = try modelContext.fetch(descriptor) } catch { print("Fetch failed: \(error)") } -
Automatic fetching with
@Query:
swift@Query(sort: \.name, order: .forward) private var games: [CodeBreaker
@Query automatically updates the results array whenever the database changes, triggering SwiftUI view updates. For dynamic queries (e.g., user-driven search), construct the @Query in the view's initializer.
Module 5: Error Handling
SwiftData operations can throw errors for various reasons (corrupted database, invalid predicate, file system errors). Use Swift's error handling system to manage these:-
do-catchblocks for full error handling:
swiftdo { let games = try modelContext.fetch(descriptor) // Use games } catch { print("Database error: \(error)") // Show error to user } -
try?for simple success/failure checks:
swiftif let games = try? modelContext.fetch(descriptor) { // Use games } else { // Handle failure } -
try!for cases where failure is impossible (use only during development):
swiftlet games = try! modelContext.fetch(descriptor)
Module 6: Model Conversion Best Practices
When converting an existing model to use SwiftData:-
Convert structs to classes for any model objects you want to persist.
-
Remove
@Observablefrom classes marked@Model. -
Handle unsupported types:
-
For enums with associated values, use a computed property that converts to/from a string or other primitive type.
-
For UI-specific types like
Color, store a hex string in the model and convert toColorin the UI layer.
-
-
Add cascade delete rules to relationships to avoid orphaned data.
-
Mark transient properties that should not be persisted (e.g., timers, temporary state).
-
Use extensions to add UI-specific functionality to your model without polluting the core model layer.
May you master SwiftData and build robust, data-driven applications with seamless persistence. May your models be well-designed, your queries be efficient, and your Assignment 5 implementation be smooth and bug-free. As you continue your iOS development journey, may you embrace clean architecture principles and create maintainable applications that scale effortlessly. Happy coding!


