Properties, Layout & @ViewBuilder - Deep dive into @State memory management, SwiftUI layout system architecture, and advanced Swift programming patterns
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:
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:
Key @State Rules:
private
- it’s only for internal View use@State private var isEditing: Bool = false
@State private var temporaryText: String = ""
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
}
}
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:
indexOfTheOneAndOnlyFaceUpCard = 5
doesn’t store “5” anywhereKey Benefits:
Comparison to Other Patterns:
When to Use Computed Properties:
get
only)get
and set
)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 }
class EmojiMemoryGame {
typealias Card = MemoryGame<String>.Card
var cards: Array<Card> { // Much cleaner than Array<MemoryGame<String>.Card>
return model.cards
}
}
SwiftUI layout follows a three-step process:
HStack Layout Algorithm:
VStack: Same as HStack but vertically
ZStack Sizing:
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)
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.
What It Does: Enables the list-building syntax in SwiftUI, converting multiple Views into a single View.
Where It’s Used:
@ViewBuilder
func conditionalContent() -> some View {
if someCondition {
Text("Condition true")
} else {
Image("alternative")
}
}
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.
Command + Click Menu:
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
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.
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.
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.