Stanford CS193p: iOS Development with SwiftUI | 2025 | L12: Even More Complex UIs

By Unknown Author

Share:

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 the pegChoices array.
  • 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 pegChoices array 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 a title, systemImage, an optional color, and an @escaping action. It utilizes .tint() for coloring and withAnimation for animated transitions.
  • Preview Functionality: A @State variable is used in the preview to provide a Binding to pegChoices, allowing the preview to dynamically update and display changes. An onChange(of:) modifier is added to the preview to log changes to pegChoices in the console.

Modal Presentation with Sheets

The lecture then addresses how to present the GameEditor modally from the GameList.

Implementing Modal Sheets

  • @State for Presentation: A @State private var showGameEditor boolean is introduced to control the presentation of the sheet.
  • .sheet() Modifier: The .sheet() modifier is attached to the "plus" button in GameList. It takes an isPresented binding (connected to showGameEditor) and a content closure that returns the view to be presented.
  • Presenting the Editor: The "plus" button's action is modified to set showGameEditor to true.
  • Passing Data to Editor: The GameEditor requires a game to edit. A @State private var gameToEdit: CodeBreaker? is created. When the "plus" button is tapped, a new CodeBreaker instance is created and assigned to gameToEdit.
  • Conditional Presentation: To prevent an empty editor from appearing, an onChange(of: gameToEdit) modifier is used to set showGameEditor to true only when gameToEdit is not nil.
  • Handling Dismissal: The .sheet() modifier's onDismiss closure is used to perform actions when the sheet is dismissed. This includes inserting the gameToEdit into the games array at the beginning of the list and resetting gameToEdit to nil.
  • Toolbar for Actions: A .toolbar modifier is added to the GameEditor to include "Cancel" and "Done" buttons.
    • Cancel Button: Sets gameToEdit to nil, which triggers the dismissal of the sheet.
    • Done Button: Calls onChoose() (a closure passed into GameEditor) and then sets gameToEdit to nil.
  • @Environment(\.dismiss): The dismiss() environment variable is used within GameEditor to programmatically dismiss the modal presentation. This simplifies the dismissal logic for both "Cancel" and "Done" actions.
  • onChoose Action Closure: A closure onChoose: () -> Void is added as an argument to GameEditor. This allows the parent view (GameList) to define what happens when the "Done" button is pressed in the editor.
  • NavigationStack for Toolbars: Toolbars are only available within a NavigationStack. Therefore, the GameList is wrapped in a NavigationStack to enable the toolbar buttons in the GameEditor.
  • Toolbar Item Placement: .toolbarItem(placement:) with .cancellationAction and .confirmationAction is used to semantically place the "Cancel" and "Done" buttons appropriately in the toolbar.
  • Refactoring with Computed Properties: The "add" and "edit" button logic in GameList and the GameEditor itself are refactored into computed properties (addButton, gameEditor) for better code organization.
  • @ViewBuilder for gameEditor: The gameEditor computed property is marked with @ViewBuilder to allow conditional logic (like if let) within its definition.

Input Validation and Alerts

The lecture emphasizes the importance of validating user input within the UI.

Implementing Input Validation

  • isValid Extension: An extension CodeBreaker is created to add an isValid computed property. This property checks if the game has a name and if it has at least two unique pegChoices (using a Set to ensure uniqueness).
  • Disabling the "Done" Button: The "Done" button in the GameEditor is disabled using the .disabled() modifier if game.isValid is false.
  • Presenting Alerts:
    • A @State private var showInvalidGameAlert boolean is introduced.
    • The .alert() modifier is attached to the "Done" button. It takes a title, an isPresented binding, and a content closure for buttons and messages.
    • The "Done" button's action now checks game.isValid. If invalid, it sets showInvalidGameAlert to true; otherwise, it proceeds with onChoose() and dismiss().
    • 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").

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 function editButton(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 new CodeBreaker instance with the same properties. This is crucial because CodeBreaker is a class (a reference type), and direct editing would modify the original object.
  • Conditional Update Logic: In the onChoose closure, the code now checks if the edited game already exists in the games array.
    • 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 attempts are 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 gameToEdit and showGameEditor: The @State variable showGameEditor is replaced with a computed Binding<Bool> that derives its value from gameToEdit != 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 get closure returns gameToEdit != nil.
    • The set closure sets gameToEdit to nil when the binding is set to false.
  • Eliminating onDismiss and onChange: With the synchronized binding, the onDismiss closure on .sheet() and the onChange(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):
    • onAppear and onDisappear: startTimer() and pauseTimer() functions are added to the CodeBreaker model. These are called using onAppear and onDisappear modifiers on the CodeBreakerView to manage the timer's lifecycle.
    • startTime and elapsedTime: The CodeBreaker model now includes startTime: Date? and elapsedTime: TimeInterval. startTime is set when the timer begins, and elapsedTime accumulates time when the timer is paused.
    • game.isOver Check: The timer is prevented from starting if the game is already over.
    • Restarting Games: When a game is restarted, startTime is reset to .now, endTime to nil, and elapsedTime to 0.
    • SystemDateOffset: A SystemFormatStyle.DateOffset is used for formatting the elapsed time.
    • scenePhase Environment 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 an onChange(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), an onChange(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 ViewModifier for Timer Tracking:
    • A custom struct ElapsedTimeTracker: ViewModifier is created to encapsulate the timer logic, including onAppear, onDisappear, scenePhase handling, and the game argument.
    • This ViewModifier is then applied using the .modifier() method, or more cleanly, via a custom extension View.trackElapsedTime(in:). This promotes code reusability and cleaner view code.

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-Powered

Load the transcript when you're ready to chat so the initial page stays lighter.

Ready to summarize another video?

Summarize YouTube Video