Note Wisdom
These annotated notes break down Stanford CS193p lecture 14, covering SwiftData migration for a Code‑Breaker app. It explains model conversion, preview fixes, @Query usage, debugging pitfalls, plus building sort and search features.
Institution: Stanford
Original Course: Stanford CS193p: iOS Development with SwiftUI | 2025 | L14: SwiftData Demonstration
Instructor Bio: This lecture is delivered by Paul Hegarty, Lecturer in Computer Science at Stanford University and the principal instructor of CS 193P since 2010. Paul Hegarty is a veteran software engineer and educator with deep roots in the Apple developer ecosystem. Earlier in his career he worked at NeXT Computer, where he contributed to the Objective-C language and the foundational tools that would eventually become Apple's modern development platform. He has taught iOS application development at Stanford for over fifteen years, guiding thousands of students through the transition from earlier UIKit frameworks to the modern SwiftUI declarative paradigm. He is widely recognized for his methodical teaching style, his emphasis on clean architectural patterns such as MVVM, and his ability to explain complex systems concepts through hands-on, live-coding demonstrations.
Course Description: This lecture is a hands-on demonstration integrating SwiftData persistence into the CodeBreaker application. It shows how to model game history records, save completed games to persistent storage, and query and display past games with complex sorting options and advanced predicates that traverse multiple database relationships. The session also covers migration considerations, undo/redo support, and best practices for keeping the persistence layer cleanly separated from the UI and business logic layers.
@Model macro. He walks through each modified source file using git diff blue change markers to recap every adjustment, because code got rearranged across many files in the prior class session.Code type changed from a struct to a class decorated with @Model. Since SwiftData only works with primitive stored types, enum values could not be saved directly to the database. The enum property kind got switched over to store raw string values. A computed property handled back‑and‑forth conversion between the string storage and the original enum, and the conversion helper code was generated with ChatGPT.Code. Structs get automatic member‑wise initializers, but classes do not. Methods like randomize and reset also lost their mutating keyword. Mutating only applies for struct value types; class methods modify instance state without needing that marker.Color is UI‑framework specific, and the goal was to fully decouple the model from UI code. All peg color storage shifted to hexadecimal string representations. A missing peg placeholder value also switched away from a Color type.CodeBreaker model class, imports shifted from SwiftUI to Foundation, fully severing UI dependencies in the model module. The peg values now stored strings instead of colors. Since CodeBreaker was already an @Observable class, adopting @Model did not require huge structural rewrites.Code objects inside each game instance. The relationship setup meant deleting a CodeBreaker game record would cascade‑delete all its associated Code entries in the separate database table.startTime property @Transient. Properties tagged transient do not persist to disk. But @Transient variables also do not trigger SwiftUI view updates when their value changes. The elapsed game timer stopped refreshing the user interface. The instructor’s quick workaround was nudging another stored model variable (elapsedTime) by a tiny floating‑point increment every time startTime was assigned. Touching a persisted model property forced SwiftUI to recognize state change and redraw views. This is a hack, not an official framework feature (3:27).CodeBreaker dropped default color‑based peg choices, since colors no longer belong in the model. When creating guess copies for game attempts, explicit new Code instances needed manual creation because classes use reference semantics instead of struct value semantics. Auto‑generated protocol conformances like Identifiable, Hashable, Equatable are supplied automatically for @Model classes, so hand‑written extensions for those could be deleted.ModelContainer in the app entry point. The container sets up the underlying SQL storage and maps @Model Swift classes to database tables.ModelContainer, preview canvases crash immediately when instantiating any @Model object (9:37)..modelContainer() as a view modifier directly inside the preview block. The preview provider itself is not a SwiftUI view, so view modifiers cannot attach to it. The instructor’s advanced solution builds a custom PreviewModifier. This is a special preview‑only construct distinct from ordinary view modifiers.makeSharedContext to spin up a fresh ModelContainer. Critically, the configuration sets isStoredInMemoryOnly: true. Preview data lives only in memory and never writes anything to disk. That avoids leftover preview junk accumulating in your local app database. A static extension on PreviewTrait creates a clean dot‑syntax calling style, so every preview can opt‑in with .previewTraits(.swiftData). The @MainActor annotation gets added to keep preview database work on the main UI thread.@Model type name. Every view preview that touches SwiftData objects needs this trait attached, otherwise it will crash. Independent small sub‑views that take plain data values and do not instantiate model objects do not need the preview trait.@State array variable in a view. That array was the source of truth. Now the database becomes source of truth. The @State array gets replaced with an @Query property.@Query fetches model objects from the database.
All contents below are exclusive to the paid Word file, NOT available on this web page
@Query updates automatically, refreshing SwiftUI views. The result collection is read‑only. You cannot append or remove items directly on the query array. All mutations must go through ModelContext.ModelContext is your handle for interacting with the database. You pull it from the environment using @Environment(\.modelContext). Important operations available are insert(), delete(), fetch(), fetchCount(), and save(). Auto‑save normally happens when the application moves to the background. When actively developing in Xcode and hitting stop on the debugger, the app does not properly background, so auto‑save never fires. Changes can get lost if you kill the running simulator that way (33:06).modelContext.delete(game) instead of removing array indices. Editing an existing game works by deleting the old instance and inserting the edited copy. Creating new games just calls insert. The old .onMove list reorder modifier got removed. Since the @Query had explicit sorting by game name, manual row reordering no longer makes sense. If you wanted manual drag‑reorder support, you would need to add a custom ordering field on your model.@Query array does not refresh instantly. Updates happen on the next SwiftUI view update cycle. Right after inserting sample game data, trying to immediately index into the games array can crash because the collection still appears empty.FetchDescriptor. @Query is the preferred high‑level tool, but sometimes you want direct manual fetches from modelContext. Fetch operations can throw errors so you need do‑catch blocks, try!, or try?. fetchCount() is more efficient than fetching full objects when you only need a total record count, as it runs purely as a SQL count operation without instantiating Swift model objects.CodeBreaker game holds an array of related Code attempt objects stored in a separate database table. When loaded back from disk, relationship collections return in random order. That completely breaks Code‑Breaker gameplay, because the sequence of user guesses matters.timestamp stored property on the Code model, recording creation date whenever a guess attempt is created. The underlying relationship property gets renamed to an underscore‑prefixed private storage variable. A public computed property wraps access: its getter always sorts the underlying collection by timestamp to guarantee chronological order. The setter assigns directly to private storage. UI code interacts exclusively with the sorted computed property and does not touch the raw unsorted storage.CaseIterable defines available sort options: sort by name, or sort by most recent attempt. A segmented Picker UI control sits at the top of the game list view for switching sort modes.@Query properties can only configure their filter and sort parameters inside their containing view’s init(). You cannot mutate sorting criteria mid‑execution inside view body code. The solution is to push the sort‑selection value down as an input argument into the game‑list view. Inside the view initializer, switch on the passed‑in sort option and construct the appropriate Query instance.lastAttemptDate optional date property to CodeBreaker. Every time a user submits a guess attempt, update this property to the current date. For newly created games with zero attempts, this value starts as nil. Nil values sort unpredictably; the instructor notes you could default it to the game creation timestamp instead..searchable() view modifier supplies a standard system search bar. The search text value flows down as another input argument to the game‑list view. Inside the initializer, a predicate filters the query results..lowercased() string methods inside the predicate closure. To implement case‑insensitive searching, pre‑compute lower‑case and capital‑cased versions of your search string outside the predicate closure and reference those constants inside the predicate.@Model, add persistence, and implement sorting plus searching features. Most of the heavy work is converting models to SwiftData. Once that is complete, inserting, deleting, and querying data becomes fairly straightforward.@Transient hack for fixing elapsed‑time UI updates is a workaround, not official framework design. There is no clean built‑in way to make transient properties trigger observation updates.save() calls to mitigate that issue for development or edge‑case production scenarios.Skip hours of watching lectures. Get organized notes, exam prep materials and problem solutions all in one Word file.
Click to see everything included
All contents below are exclusive to the paid Word file, NOT available on this web page

