Jeffrey Hicks

Jeffrey Hicks

Platform Eng @R360

Stanford CS193p 2023: Lecture 05 Notes

Properties, Layout & @ViewBuilder - Deep dive into @State memory management, SwiftUI layout system architecture, and advanced Swift programming patterns

By Stanford CS193p • Jan 9, 2025
Lecture 5: Properties, Layout & @ViewBuilder
by Stanford CS193p
Published Sep 1, 2023

Comprehensive lecture covering fundamental SwiftUI concepts around property management, layout systems, and advanced Swift features

This comprehensive lecture covers fundamental SwiftUI concepts around property management, layout systems, and advanced Swift features. Here’s everything you need to know:

@State Property Wrapper Deep Dive

Understanding @State Memory Management

Why Views Are Read-Only: SwiftUI Views are constantly created and destroyed, but their bodies persist on screen. Views are designed to be stateless and should only display what’s in the model.

When to Use @State:

  • Temporary UI state during editing modes
  • Tracking whether auxiliary views are displayed
  • Animation endpoint tracking
  • Internal View workings only

Key @State Rules:

  • Always mark as private - it’s only for internal View use
  • Causes View body rebuilds when state changes
  • Memory persists for the lifetime of the View’s body on screen, not the struct
  • Acts as a pointer to memory space that survives View struct recreation
@State private var isEditing: Bool = false
@State private var temporaryText: String = ""

Access Control Best Practices

Private and Private(set) Usage

Core Principle: Mark everything private by default, only remove when external access is needed.

Model Layer Protection:

struct MemoryGame<CardContent> {
    private(set) var cards: Array<Card>  // Readable but not settable
    private var indexOfTheOneAndOnlyFaceUpCard: Int?  // Completely private
    
    mutating func choose(_ card: Card) {  // Public interface
        // Implementation
    }
}

ViewModel Gatekeeper Pattern:

class EmojiMemoryGame: ObservableObject {
    private var model = createMemoryGame()  // Model hidden from Views
    
    var cards: Array<MemoryGame<String>.Card> {
        return model.cards
    }
    
    func choose(_ card: MemoryGame<String>.Card) {
        model.choose(card)
    }
    
    private static func createMemoryGame() -> MemoryGame<String> {
        // Factory method
    }
}

Advanced Swift Programming Techniques

Computed Properties with Get/Set

Transform stored properties into computed ones to eliminate data duplication bugs and create “smart” properties that enforce business logic:

var indexOfTheOneAndOnlyFaceUpCard: Int? {
    get {
        let faceUpCardIndices = cards.indices.filter { cards[$0].isFaceUp }
        return faceUpCardIndices.count == 1 ? faceUpCardIndices.first : nil
    }
    set {
        for index in cards.indices {
            cards[index].isFaceUp = (index == newValue)
        }
    }
}

Understanding the Power of Setters as Hooks:

The set in a computed property acts like a “hook” or custom handler—it lets you define exactly what should happen when a value is assigned to the property. Instead of just storing the value, it can trigger any custom logic you need.

In this example:

  • Setting indexOfTheOneAndOnlyFaceUpCard = 5 doesn’t store “5” anywhere
  • Instead, it triggers logic that turns all cards face down, then turns card 5 face up
  • The setter enforces the rule that only one card can be face up at a time

Key Benefits:

  • No Data Duplication: The index is computed from the cards array, not stored separately
  • Automatic Consistency: Can’t have mismatched state between the index and actual face-up cards
  • Business Logic Enforcement: The setter ensures game rules are always followed
  • Intercepted Assignments: Any attempt to set the property runs your custom logic

Comparison to Other Patterns:

  • Similar to property setter methods in Java/C#
  • Acts as an observer for assignment that runs code whenever the property changes
  • More powerful than plain stored properties because you control both read and write behavior

When to Use Computed Properties:

  • When a value can be derived from other state (use get only)
  • When setting a value should trigger side effects or enforce rules (use get and set)
  • To maintain consistency between related pieces of state
  • To create a simpler API that hides complex implementation details

Functional Programming with Extensions

Adding Functionality to Built-in Types:

extension Array {
    var oneAndOnly: Element? {
        return count == 1 ? first : nil
    }
}

// Usage
let faceUpCardIndices = cards.indices.filter { cards[$0].isFaceUp }
return faceUpCardIndices.oneAndOnly

Filter Function for Functional Style:

// Instead of loops, use functional programming
let faceUpCardIndices = cards.indices.filter { cards[$0].isFaceUp }

Type Aliases for Code Cleanup

class EmojiMemoryGame {
    typealias Card = MemoryGame<String>.Card
    
    var cards: Array<Card> {  // Much cleaner than Array<MemoryGame<String>.Card>
        return model.cards
    }
}

Layout System Architecture

The Layout Process

SwiftUI layout follows a three-step process:

  1. Container Views offer space to their child Views
  2. Views choose their size within the offered space
  3. Container Views position the child Views

HStack, VStack, and ZStack Behavior

HStack Layout Algorithm:

  • Offers space to least flexible Views first
  • Divides remaining space among flexible Views
  • Views can be inflexible, flexible, or very flexible

VStack: Same as HStack but vertically

ZStack Sizing:

  • Sizes itself to fit all children
  • If any child is fully flexible, ZStack becomes fully flexible
  • All children get the same space offering

Layout Modifiers vs Container Views

Background and Overlay vs ZStack:

// ZStack - both Views affect sizing
ZStack {
    Rectangle()
    Text("Hello")
}

// Background - only Text affects sizing
Text("Hello")
    .background(Rectangle())

// Overlay - only Circle affects sizing  
Circle()
    .overlay(Text("Hello"), alignment: .center)

Padding Modifier Layout:

Text("Hello")
    .padding(10)
  • Takes offered space, subtracts 10 points from all edges
  • Offers remaining space to Text
  • Adds 10 points back to Text’s chosen size

GeometryReader for Adaptive Sizing

When Views need to know their available space:

GeometryReader { geometry in
    Text("Hello")
        .font(.system(size: min(geometry.size.width, geometry.size.height) * 0.1))
}

Critical Rule: GeometryReader always uses all offered space - it’s extremely flexible.

@ViewBuilder Deep Dive

Understanding @ViewBuilder

What It Does: Enables the list-building syntax in SwiftUI, converting multiple Views into a single View.

Where It’s Used:

  • Container View content (HStack, VStack, ZStack)
  • View modifiers that take View parameters
  • Custom functions that build Views
@ViewBuilder
func conditionalContent() -> some View {
    if someCondition {
        Text("Condition true")
    } else {
        Image("alternative")
    }
}

Conditional Views

If-Else in @ViewBuilder:

VStack {
    Text("Always visible")
    if isLoggedIn {
        Text("Welcome back!")
    } else {
        Button("Log In") { }
    }
}

Important Limitation: No variables allowed in @ViewBuilder contexts - only View-creating statements.

Practical Development Tools

Xcode Features Demonstrated

Command + Click Menu:

  • Rename: Safely rename symbols across entire project
  • Jump to Definition: Navigate to symbol definitions
  • Fold: Collapse code sections for better organization
  • Quick Help: View documentation and type information

Code Folding: Use Editor → Code Folding to collapse methods, functions, and comment blocks.

Type Inference Best Practices:

// Prefer this
let emojis = ["👻", "🎃", "🕷️"]
var isFaceUp = false

// Over this
let emojis: Array<String> = ["👻", "🎃", "🕷️"]  
var isFaceUp: Bool = false

Advanced Container Views

Specialized Containers

LazyVStack/LazyHStack: Only create Views as needed for performance with large datasets.

List: Automatically scrollable, great for data collections with built-in styling.

ScrollView: Manual scrolling container, combines well with LazyVStack.

Form: Optimized for user input with platform-appropriate styling.

View Modifier Chaining

Order Matters:

Text("Hello")
    .foregroundColor(.white)
    .padding()
    .background(Color.blue)
// vs
Text("Hello") 
    .padding()
    .background(Color.blue)
    .foregroundColor(.white)

The first applies blue background to the padded text, the second might not work as expected.

Memory Game Implementation Improvements

Robust Card Selection Logic

The lecture demonstrates refactoring the memory game’s card selection to use computed properties and functional programming:

mutating func choose(_ card: Card) {
    if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
       !cards[chosenIndex].isFaceUp,
       !cards[chosenIndex].isMatched {
        
        if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
            if cards[chosenIndex].content == cards[potentialMatchIndex].content {
                cards[chosenIndex].isMatched = true
                cards[potentialMatchIndex].isMatched = true
            }
            cards[chosenIndex].isFaceUp = true
        } else {
            indexOfTheOneAndOnlyFaceUpCard = chosenIndex
        }
    }
}

This approach eliminates potential bugs from storing the same information in multiple places by computing indexOfTheOneAndOnlyFaceUpCard from the cards array rather than maintaining it separately.

The lecture emphasizes progressive code improvement, functional programming patterns, and defensive programming practices that make SwiftUI applications more robust and maintainable.

Related

#swiftui