Stanford CS193p: iOS Development with SwiftUI | 2025 | L4: CodeBreaker's Model

Unknown AuthorAbout 9 min readNov 26, 2025Watch original
THE SUMMARYAI-generated

Key Concepts

  • Model-View-Controller (MVC) / Model-View-ViewModel (MVVM) Separation: The core principle of separating application logic (Model) from user interface (UI/View).
  • Structs as Models: Using Swift structs to define the data and logic of the application's core game.
  • Value Types vs. Reference Types: Understanding how structs (value types) are copied and how classes (reference types) are shared.
  • mutating func: A keyword required for methods within structs that modify the struct's properties.
  • @State Property Wrapper: A SwiftUI mechanism to declare mutable state within a View struct, allowing the UI to react to changes.
  • Optionals: Swift's way of handling the absence of a value, often used with if let or the nil coalescing operator.
  • Equatable Protocol: A protocol that allows types to be compared for equality using the == operator.
  • typealias: An alias for an existing type, used for clarity and potential future refactoring.
  • Namespacing: Organizing types within other types (e.g., Code.Kind) to avoid naming conflicts and improve readability.
  • onTapGesture View Modifier: Attaching an action to a tap event on a SwiftUI View.
  • ScrollView: A SwiftUI container View that enables scrolling for its content.
  • withAnimation: A SwiftUI function to apply default animations to UI changes.
  • contentShape View Modifier: Defining the interactive area of a View, especially useful for transparent elements.
  • init (Initializer): A special method used to create and initialize an instance of a struct or class.
  • self. Keyword: Used to disambiguate between a property and a parameter with the same name, or to refer to the current instance.

Model-UI Separation: The CodeBreaker Game

The lecture focuses on separating the Model from the UI for the CodeBreaker game, aiming for a fully implemented gameplay by the end. The UI is described as a "manifestation of our Model," emphasizing that the Model holds the core logic and data, independent of how it's displayed.

Creating the Model

The Model is built using Swift structs, which are value types.

  • CodeBreaker Struct: This is the main game Model.
    • It contains var masterCode: Code, var guess: Code, var attempts: [Code], and let pegChoices: [Peg].
    • pegChoices is made a let because it's not expected to change during the game.
    • The AI's suggestion for makeGuess() highlights the intended functionality.
  • Code Struct: Represents a sequence of pegs.
    • Contains var pegs: [Peg].
    • Includes an inner enum Kind for namespacing: case masterCode, case guess, case attempt. This distinguishes the purpose of a Code instance.
    • The Kind enum is later enhanced with associated data for the attempt case: case attempt([Match]), to store the match results directly with the attempt.
    • A computed var matches: [Match] is added to Code to conveniently access the associated match data, returning an empty array for masterCode or guess kinds.
  • Peg Typealias: Initially considered a struct, Peg is simplified to typealias Peg = Color.
    • This decision is made for simplicity, acknowledging that Color is fundamentally a UI concept, temporarily breaking the Model's UI-independence.
    • The instructor notes this will be addressed in homework by changing Peg to String (e.g., "green", "smiley face emoji"), with the UI responsible for rendering the String as a Color or emoji.
    • Code.missing: A static let missing: Peg = .clear is added to Code to represent an unset or missing peg, using .clear for visual transparency.

Connecting the Model to the UI

The ContentView is refactored and renamed to CodeBreakerView to reflect its specific purpose of displaying the CodeBreaker game.

  • Instantiating the Model in the View: A let game: CodeBreaker is added to CodeBreakerView.
    • Initially, this requires providing default values for all CodeBreaker properties (masterCode, guess, attempts, pegChoices) during instantiation, as Swift structs require all properties to be initialized.
    • The masterCode and guess are initially hardwired to [.green, .red, .red, .yellow] for testing.
    • attempts starts as an empty array [].
    • pegChoices is initialized with [.red, .green, .blue, .yellow].
  • Refactoring with Xcode: The instructor demonstrates Xcode's "Refactor > Rename" feature to safely rename ContentView to CodeBreakerView across the entire project, including file names, comments, and references.
  • Displaying Model Data: The UI elements (pegs) are updated to display data from game.masterCode.pegs and game.guess.pegs instead of hardwired test data.
  • view(for code: Code) Function: A helper function is created within CodeBreakerView to generate a View for a given Code instance, encapsulating the display logic for a single row of pegs. This is suggested to eventually become a dedicated CodeView struct.

Implementing User Interaction: Tapping Pegs

The ability to change guess pegs by tapping them is implemented.

  • onTapGesture: The .onTapGesture View modifier is added to the RoundedRectangle that draws each peg.
    • The closure within onTapGesture checks if code.kind == .guess to ensure only guess pegs are interactive.
    • It then calls game.changeGuessPeg(at: index) on the Model.
  • changeGuessPeg(at index: Int) in Model:
    • This func in CodeBreaker is responsible for cycling the peg at the given index through pegChoices.
    • It retrieves existingPeg = guess.pegs[index].
    • It uses pegChoices.firstIndex(of: existingPeg) to find the current peg's position. This returns an Optional<Int>.
    • if let for Optionals: if let indexOfExistingPegInPegChoices = pegChoices.firstIndex(of: existingPeg) is used to safely unwrap the optional index.
    • Modulo Operator (%): (indexOfExistingPegInPegChoices + 1) % pegChoices.count is used to cycle through the pegChoices array, wrapping around to the beginning.
    • nil coalescing operator (??): In the else case (if existingPeg is not found in pegChoices), guess.pegs[index] = pegChoices.first ?? Code.missing is used. This attempts to set the peg to the first pegChoice, or Code.missing if pegChoices is empty.
    • mutating func: Since changeGuessPeg modifies guess.pegs, it must be marked as mutating func.
  • @State for Mutable Model in View: The game property in CodeBreakerView is changed from let to @State var game: CodeBreaker.
    • This is crucial because structs are immutable by default. A View itself is a struct and its body is a read-only computed property, so it cannot directly call mutating funcs on its own properties.
    • @State creates a hidden, mutable storage for game on the heap, allowing the View to modify it and trigger UI updates. It marks game as a "source of truth" for mutable state within the View.

Implementing Game Logic: Guessing

A "Guess" button is added to submit the current guess.

  • Button View: A Button with the text "Guess" is added.
    • Its action closure calls game.attemptGuess() on the Model.
  • attemptGuess() in Model:
    • This mutating func in CodeBreaker adds the current guess to the attempts array.
    • Value Type Copying: var attempt = guess creates a copy of the guess struct.
    • attempt.kind = .attempt(guess.match(against: masterCode)) then modifies only this copy, setting its kind to .attempt and associating the match results.
    • attempts.append(attempt) adds the modified copy to the attempts array.
    • This demonstrates the advantage of value types: modifying a copy doesn't affect the original guess.
    • Automatic Reference Counting (ARC): The instructor clarifies that Swift uses ARC for classes, not garbage collection, and structs don't require ARC as they are stored directly in memory.
  • Displaying Attempts: The attempts array is displayed using a ForEach loop: ForEach(game.attempts.indices.reversed(), id: \.self) { index in view(for: game.attempts[index]) }.
    • .reversed() is used to show the most recent attempt at the top, near the current guess.
    • id: \.self is used with indices because the indices are unique, even if the Code instances themselves are not.

UI Enhancements

Several UI improvements are made for better user experience.

  • ScrollView: The ForEach for attempts and the guess button are wrapped in a ScrollView.
    • This prevents content from pushing off-screen and keeps the guess button in a fixed position relative to the scrollable content.
  • withAnimation: withAnimation { game.attemptGuess() } is used around the attemptGuess() call in the button's action.
    • This provides smooth animations (e.g., new attempts fading in, existing attempts sliding down) when the UI updates.
  • Match Markers for Attempts: The MatchMarkers for attempts are updated to use the code.matches property, which extracts the associated data from Code.Kind.attempt.
    • This ensures match markers are only shown for attempt codes and are empty for masterCode and guess codes.
  • Guess Button Placement: The guess button is moved to overlay the MatchMarkers area of the current guess.
    • if code.kind == .guess { guessButton } is used within the overlay modifier to conditionally display the button.
    • font(.system(size: 80)) and .minimumScaleFactor(0.1): These modifiers are applied to the Button to make its text large but scale down if necessary to fit the available space.

Advanced Model Features

  • Random Master Code:
    • A mutating func randomize(from pegChoices: [Peg]) is added to the Code struct.
    • It uses a for index in pegs.indices loop and pegChoices.randomElement() ?? Code.missing to assign random pegs.
    • This randomize function is called within the CodeBreaker's init (initializer).
  • init for CodeBreaker:
    • An init(pegChoices: [Peg] = [.red, .green, .blue, .yellow]) is added to CodeBreaker.
    • This allows the pegChoices to be customized when creating a CodeBreaker instance, with a default provided.
    • self.pegChoices = pegChoices: The self. keyword is used to distinguish the instance property pegChoices from the initializer parameter pegChoices.
    • The masterCode is initialized and then masterCode.randomize(from: self.pegChoices) is called to set a random master code.
    • A print(masterCode) statement is added for debugging/cheating.
  • Missing Pegs and contentShape:
    • The Code initializer is changed to pegs: Array(repeating: Code.missing, count: 4) so guess pegs start as clear.
    • Because .clear pegs don't receive touch events, contentShape(Rectangle()) is added as a View modifier to the RoundedRectangle drawing the pegs. This forces the transparent area to be interactive.
    • An overlay is added to draw a gray RoundedRectangle border around Code.missing pegs, making them visible as clickable areas.

Equatable Protocol for Code.Kind

  • The compiler requires Code.Kind to conform to Equatable when using == with associated data.
  • By adding enum Kind: Equatable { ... }, Swift can automatically synthesize the == implementation because all its associated data ([Match]) is also Equatable. Array is Equatable if its elements are Equatable, and Match is an enum without associated data, making it Equatable by default.

Conclusion and Homework

The lecture successfully demonstrates the separation of Model and UI, implementing core game mechanics and UI interactions. The final UI allows users to tap pegs to change their colors, submit guesses, and view attempts with match markers, all with basic animations.

Homework Assignment 2: Students are tasked with extending the game to support both colors and emojis for pegs. This requires changing typealias Peg = String and implementing logic to convert String representations to Colors or display emojis in the UI. AI tools are suggested for assistance with this conversion.

AI summaries can miss context or contain errors. Check important details against the original video.

MAKE IT YOURS

Read. Remember. Reuse.

Free tools

Go a little deeper.

Have a question about this video? Load its transcript to open the video chat.