Note Wisdom
These annotated notes for CS193p L12 explain SwiftUI complex UI techniques: reusable views, sheets, validation alerts, custom bindings, timer tracking, and view modifiers, including tricky edge‑cases across iPhone and iPad.
Institution: Stanford
Original Course: Stanford CS193p: iOS Development with SwiftUI | 2025 | L12: Even More Complex UIs
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 advances into more sophisticated UI patterns and interactive components. It covers tab views for multi-section applications, searchable interfaces, context menus, and custom view components that encapsulate reusable UI logic. The session also explores advanced state management patterns for coordinating data across deeply nested view hierarchies, and demonstrates how to build rich, interactive interfaces that go beyond standard system components while remaining maintainable and performant.
PegChoicesChooser. This view manages the list of peg color options. The reasoning here is straightforward: the main game editor should not own every piece of nested UI. Isolating this sub‑view makes previewing easier and keeps responsibilities narrow (1:00‑3:00).@Binding and @Bindable. @Binding is used for struct value‑type data passed between views. @Bindable works with reference‑type ObservableObject classes, letting you generate a $ binding to mutate the observed model. The instructor emphasizes a good habit: avoid passing an entire model object into a sub‑view if only one property is needed. For PegChoicesChooser, only the array of pegs is passed, not the whole game object.@escaping because the closure will get stored and run later when the user taps, instead of executing immediately during view construction. They also use the tint() view modifier to apply custom coloring. Green tint goes to add, red tint to delete. All button actions get wrapped inside withAnimation so inserting and deleting list rows animates smoothly (6:00‑10:00).@State variable. Since the preview needs direct access to that state variable, the property uses @Previewable and cannot be marked private. They also add an onChange modifier inside the preview to print changes to peg choices, so they can verify edits are working in the preview canvas without running the full app.sheet modifier for modal presentation (11:00‑14:00).isPresented value. When true, sheet appears; when false, sheet disappears. Sheets animate automatically. You do not need to wrap state changes in withAnimation to trigger the sheet transition.@State variable showGameEditor. Then they realize they need to track which game object is being edited. They switch to an optional game object gameToEdit. If this optional is non‑nil, the sheet should show.onChange(of:) to sync gameToEdit to the boolean presentation state. When gameToEdit becomes non‑nil, set showGameEditor to true. When it becomes nil, set it false. They also use sheet’s onDismiss callback to clean up state when the sheet closes.gameToEdit is optional, but the GameEditor view expects a non‑optional game argument. Inside the sheet’s view‑builder they use if‑let conditional unwrapping. The instructor notes that force‑unwrapping (!) could work during development but should be avoided for shipping code. If the optional were nil, the sheet would render empty content, which is a bad user experience.NavigationStack, so they embed the GameEditor inside one even though they are not doing actual navigation. They use ToolbarItem with semantic placements: .cancellationAction for Cancel and .confirmationAction for Done. SwiftUI automatically positions Cancel to the left and bolds Done on the right side.GameEditor. They add a callback closure parameter onChoose that runs when the user taps Done. Inside the editor they use the environment value @Environment(\.dismiss). Calling this dismiss function closes the sheet from within the modal view itself. This removes some of the state‑sync burden from the parent view.
All contents below are exclusive to the paid Word file, NOT available on this web page
isValid property as an extension on the model, treated as UI‑layer code..disabled() modifier. The instructor thinks this is not great UX. Users get confused when a button is greyed out with zero feedback explaining why it is disabled.alert. Alerts work similarly to sheets: they are view modifiers bound to a boolean presentation state. The alert displays a title, descriptive message, and confirmation button. The alert explains exactly what requirements are not satisfied, so users know what needs fixing (34:00‑41:00)..swipeActions) for the leading edge of list rows. A caveat is mentioned: swipe actions do not work on macOS, so context menus remain necessary for cross‑platform compatibility.Codebreaker is a class (reference type), passing the real game object into the editor means all edits mutate the original immediately. If the user taps Cancel, changes are already applied, which is wrong.gameToEdit and boolean showGameEditor. These two variables represent the same logical state, which creates maintenance overhead. They show how to construct a custom Binding using the get and set closure initializer.get returns gameToEdit != nil. The set only responds to being set to false (dismiss case), setting gameToEdit = nil. Setting the binding to true is ignored because there is no meaningful value to assign. This eliminates the separate boolean state variable and removes the need for the onChange sync and some onDismiss logic (48:00‑54:00).onAppear / onDisappear, but on iPad split view, views stay in memory and do not disappear when selection changes. So onAppear / onDisappear alone cannot reliably start and pause timers.startTimer() and pauseTimer(). Inside the model they store:
startTime: Date?elapsedTime: TimeIntervalstartTime, add it to elapsedTime, then set startTime back to nil. When starting, assign current date to startTime. Restarting a game resets both values. When the game finishes, the timer pauses automatically.onChange(of: game) with both old and new value parameters. When the selected game changes, pause the old game’s timer and start the new game’s timer.@Environment(\.scenePhase) environment variable. scenePhase can be .active, .inactive, .background. Using onChange(of: scenePhase), pause timers when moving to background and resume when becoming active. .inactive transitional state gets ignored.ViewModifier. A ViewModifier struct has a body(content: Content) method. The content parameter represents whatever view the modifier attaches to. The modifier can access environment values like scenePhase, then apply any number of view modifiers to content. They then add a convenience method on View so you can write something like .trackElapsedTime(inGame: game) instead of instantiating the modifier struct directly.scenePhase transition to .inactive does not trigger timer changes, so timing will have small inaccuracies during app state transitions. They accept this as acceptable trade‑off for practical code.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

