My notes from Lecture 2 covering @State, layout components, Swift type system, and building a memory card game with SwiftUI
Screen capture of Lecture 2 of Stanford's CS193p course (Developing iOS Applications using SwiftUI) from Spring quarter 2023. More SwiftUI basics.
This lecture covers essential SwiftUI concepts and Swift programming techniques through building a memory card game. Below are the key concepts and techniques demonstrated.
Views are immutable structures - this is a fundamental principle of SwiftUI. When you create a ContentView: View
, you’re declaring that ContentView behaves like a View through protocol conformance, not inheritance.
struct ContentView: View {
var body: some View {
// View content here
}
}
The some View
return type uses type inference to let the compiler determine the actual View type being returned (like Text
, VStack
, or complex nested types). This eliminates the need to specify complex return types like VStack<TupleView<(Text, Text)>>
.
Since Views are immutable, you need @State
for temporary state that can change within a View:
struct CardView: View {
@State var isFaceUp = false
var body: some View {
// Can modify isFaceUp with @State
Button("Flip") {
isFaceUp.toggle() // Bool has a toggle() function
}
}
}
Important: @State
is only for temporary display state, not for core application data. The @State
property wrapper creates a pointer to memory where the actual value is stored, allowing the underlying value to change while keeping the View immutable.
VStack, HStack, and ZStack are the primary layout containers:
VStack {
Text("Top")
Text("Bottom")
}
HStack {
Text("Left")
Spacer() // Fills available space
Text("Right")
}
ZStack(alignment: .top) { // Can specify alignment
RoundedRectangle(cornerRadius: 10)
Text("Overlay")
}
Since ViewBuilder doesn’t support traditional for
loops, use ForEach
:
let emojis = ["🎃", "👻", "🕷️", "😈"]
HStack {
ForEach(0..<4, id: \.self) { index in
CardView(content: emojis[index])
}
}
The id: \.self
parameter is required for ranges (explanation deferred to future lectures).
Swift is extremely strongly typed but uses extensive type inference to reduce verbosity:
let base = RoundedRectangle(cornerRadius: 10) // Type inferred
// Instead of: let base: RoundedRectangle = RoundedRectangle(cornerRadius: 10)
Use Option+click on variables to see their inferred types.
let
: Creates constants that never changevar
: For values that will varyBest practice: Start with let
and change to var
only when the compiler complains.
Arrays use generic syntax to specify the element type:
let emojis: [String] = ["🎃", "👻", "🕷️"] // Preferred syntax
let emojis: Array<String> = ["🎃", "👻", "🕷️"] // Also valid
// Indexing
let firstEmoji = emojis[0] // Array bounds checking - crashes if out of range
When the last parameter is a function, you can use trailing closure syntax:
// Standard syntax
ZStack(alignment: .center, content: {
// Views here
})
// Trailing closure
ZStack(alignment: .center) {
// Views here
}
// With default alignment, can omit parentheses entirely
ZStack {
// Views here
}
ViewBuilder supports three main constructs:
if
statements and switch
let
declarations only (no mutation like x = x + 1
)var body: some View {
let base = RoundedRectangle(cornerRadius: 10) // ✅ Allowed
VStack {
if someCondition {
Text("Conditional content")
}
base.fill(.blue)
base.stroke(.red)
}
}
Add tap gestures using the onTapGesture
view modifier:
ZStack {
// Card content
}
.onTapGesture {
// This is normal Swift code, not ViewBuilder
print("Card tapped")
isFaceUp.toggle()
}
The closure inside onTapGesture
is normal Swift code where you can perform any operations.
Shapes like RoundedRectangle
and Circle
have default behaviors:
RoundedRectangle(cornerRadius: 10) // Fills by default
.fill(.white) // Explicit fill with color
.strokeBorder(.red) // Stroke the border
If you don’t specify .fill()
or .stroke()
, shapes will fill by default.
print()
output in Xcode 14.3+Use print()
statements extensively for debugging declarative UI, especially in the first few weeks of learning SwiftUI. The declarative nature can make traditional step-through debugging more challenging than imperative code.
This lecture establishes the foundation for building interactive SwiftUI applications while emphasizing the importance of understanding Swift’s type system and SwiftUI’s declarative, immutable architecture.