Stanford CS193p: iOS Development with SwiftUI | 2025 | L12: Even More Complex UIs
By Unknown Author
Key Concepts
- View Modifiers:
.sheet(),.toolbar(),.contextMenu(),.swipeActions(),.disabled(),.alert(),.modifier() - State Management:
@State,@Binding,@Bindable,@Observable,@Environment - ViewBuilder:
@ViewBuilder - Data Structures:
Array,Set - Control Flow:
if let,ForEach,switch - Animation:
withAnimation - Time Management:
Date,TimeInterval,Timer - App Lifecycle:
onAppear,onDisappear,scenePhase
PegChoicesChooser and GameEditor Enhancements
The lecture begins by refactoring the GameEditor view to extract the PegChoicesChooser into its own reusable view. This new PegChoicesChooser takes a @Binding to an array of Peg objects, allowing it to display, add, and remove peg choices.
PegChoicesChooser Functionality
- Adding Pegs: A "Add Peg" button appends a new default peg (e.g.,
.green) to thepegChoicesarray. - Removing Pegs: Each peg choice is made into a button with a "minus.circle" system image. Tapping this button removes the corresponding peg from the
pegChoicesarray using its index. - Custom Button Creation: To improve the UI and differentiate between add and remove actions, a custom
button()function is created. This function takes atitle,systemImage, an optionalcolor, and an@escapingaction. It utilizes.tint()for coloring andwithAnimationfor animated transitions. - Preview Functionality: A
@Statevariable is used in the preview to provide aBindingtopegChoices, allowing the preview to dynamically update and display changes. AnonChange(of:)modifier is added to the preview to log changes topegChoicesin the console.
Modal Presentation with Sheets
The lecture then addresses how to present the GameEditor modally from the GameList.
Implementing Modal Sheets
@Statefor Presentation: A@State private var showGameEditorboolean is introduced to control the presentation of the sheet..sheet()Modifier: The.sheet()modifier is attached to the "plus" button inGameList. It takes anisPresentedbinding (connected toshowGameEditor) and acontentclosure that returns the view to be presented.- Presenting the Editor: The "plus" button's action is modified to set
showGameEditortotrue. - Passing Data to Editor: The
GameEditorrequires agameto edit. A@State private var gameToEdit: CodeBreaker?is created. When the "plus" button is tapped, a newCodeBreakerinstance is created and assigned togameToEdit. - Conditional Presentation: To prevent an empty editor from appearing, an
onChange(of: gameToEdit)modifier is used to setshowGameEditortotrueonly whengameToEditis notnil. - Handling Dismissal: The
.sheet()modifier'sonDismissclosure is used to perform actions when the sheet is dismissed. This includes inserting thegameToEditinto thegamesarray at the beginning of the list and resettinggameToEdittonil. - Toolbar for Actions: A
.toolbarmodifier is added to theGameEditorto include "Cancel" and "Done" buttons.- Cancel Button: Sets
gameToEdittonil, which triggers the dismissal of the sheet. - Done Button: Calls
onChoose()(a closure passed intoGameEditor) and then setsgameToEdittonil.
- Cancel Button: Sets
@Environment(\.dismiss): Thedismiss()environment variable is used withinGameEditorto programmatically dismiss the modal presentation. This simplifies the dismissal logic for both "Cancel" and "Done" actions.onChooseAction Closure: A closureonChoose: () -> Voidis added as an argument toGameEditor. This allows the parent view (GameList) to define what happens when the "Done" button is pressed in the editor.NavigationStackfor Toolbars: Toolbars are only available within aNavigationStack. Therefore, theGameListis wrapped in aNavigationStackto enable the toolbar buttons in theGameEditor.- Toolbar Item Placement:
.toolbarItem(placement:)with.cancellationActionand.confirmationActionis used to semantically place the "Cancel" and "Done" buttons appropriately in the toolbar. - Refactoring with Computed Properties: The "add" and "edit" button logic in
GameListand theGameEditoritself are refactored into computed properties (addButton,gameEditor) for better code organization. @ViewBuilderforgameEditor: ThegameEditorcomputed property is marked with@ViewBuilderto allow conditional logic (likeif let) within its definition.
Input Validation and Alerts
The lecture emphasizes the importance of validating user input within the UI.
Implementing Input Validation
isValidExtension: Anextension CodeBreakeris created to add anisValidcomputed property. This property checks if the game has a name and if it has at least two uniquepegChoices(using aSetto ensure uniqueness).- Disabling the "Done" Button: The "Done" button in the
GameEditoris disabled using the.disabled()modifier ifgame.isValidisfalse. - Presenting Alerts:
- A
@State private var showInvalidGameAlertboolean is introduced. - The
.alert()modifier is attached to the "Done" button. It takes a title, anisPresentedbinding, and acontentclosure for buttons and messages. - The "Done" button's action now checks
game.isValid. If invalid, it setsshowInvalidGameAlerttotrue; otherwise, it proceeds withonChoose()anddismiss(). - The alert message provides specific feedback on why the game is invalid (e.g., "A game must have a name and more than one unique peg").
- A
Editing Existing Games and Data Copying
The process of editing existing games is refined to prevent unintended data modifications.
Editing Existing Games
editButton(for: game)Function: A new functioneditButton(for: game: CodeBreaker)is created to generate an "Edit" button for context menus or swipe actions.- Copying Game Data: To prevent live editing of games already in the list, a copy of the game is made before presenting the
GameEditor. This is done by creating a newCodeBreakerinstance with the same properties. This is crucial becauseCodeBreakeris a class (a reference type), and direct editing would modify the original object. - Conditional Update Logic: In the
onChooseclosure, the code now checks if the edited game already exists in thegamesarray.- If it exists, the original game is replaced with the edited copy.
- If it's a new game (from the "+" button), it's inserted at the beginning of the list.
- Resetting Attempts on Edit: A side effect of editing and saving an existing game is that its
attemptsare reset to zero. This is considered correct behavior as changing the pegs effectively restarts the game.
Advanced Swift Concepts
The lecture delves into more advanced Swift features for cleaner code and better state management.
Advanced Swift Features
- Binding
gameToEditandshowGameEditor: The@StatevariableshowGameEditoris replaced with a computedBinding<Bool>that derives its value fromgameToEdit != nil. This eliminates the need for separate state management for the sheet's presentation and ensures synchronization.- The
Binding(get: set:)initializer is used to create this computed binding. - The
getclosure returnsgameToEdit != nil. - The
setclosure setsgameToEdittonilwhen the binding is set tofalse.
- The
- Eliminating
onDismissandonChange: With the synchronized binding, theonDismissclosure on.sheet()and theonChange(of: gameToEdit)modifier are no longer necessary. .swipeActions(): The lecture demonstrates how to implement swipe-to-edit functionality using.swipeActions()on the leading edge of list items, similar to the existing swipe-to-delete.- Timer Management (
elapsedTime):onAppearandonDisappear:startTimer()andpauseTimer()functions are added to theCodeBreakermodel. These are called usingonAppearandonDisappearmodifiers on theCodeBreakerViewto manage the timer's lifecycle.startTimeandelapsedTime: TheCodeBreakermodel now includesstartTime: Date?andelapsedTime: TimeInterval.startTimeis set when the timer begins, andelapsedTimeaccumulates time when the timer is paused.game.isOverCheck: The timer is prevented from starting if the game is already over.- Restarting Games: When a game is restarted,
startTimeis reset to.now,endTimetonil, andelapsedTimeto0. SystemDateOffset: ASystemFormatStyle.DateOffsetis used for formatting the elapsed time.scenePhaseEnvironment Variable: The@Environment(\.scenePhase)variable is used to pause the timer when the app enters the background and resume it when it becomes active. This is handled with anonChange(of: scenePhase)modifier.onChange(of: game)with Old and New Values: To handle game switching on iPad (where views don't disappear/appear in the same way), anonChange(of: game)modifier is used. This modifier can provide both the old and new game values, allowing for pausing the old game's timer and starting the new one.
- Custom
ViewModifierfor Timer Tracking:- A custom
struct ElapsedTimeTracker: ViewModifieris created to encapsulate the timer logic, includingonAppear,onDisappear,scenePhasehandling, and thegameargument. - This
ViewModifieris then applied using the.modifier()method, or more cleanly, via a custom extensionView.trackElapsedTime(in:). This promotes code reusability and cleaner view code.
- A custom
This comprehensive summary covers the key technical details, architectural decisions, and Swift features demonstrated in the lecture, providing a deep dive into the development process.
Chat with this Video
AI-PoweredLoad the transcript when you're ready to chat so the initial page stays lighter.


