SwiftUI Architecture
When to Use This Skill
Use this skill when:
- You have logic in your SwiftUI view files and want to extract it
- Choosing between MVVM, TCA, vanilla SwiftUI patterns, or Coordinator
- Refactoring views to separate concerns
- Making SwiftUI code testable
- Asking "where should this code go?"
- Deciding which property wrapper to use (@State, @Environment, @Bindable)
- Organizing a SwiftUI codebase for team development
Example Prompts
| What You Might Ask |
Why This Skill Helps |
| "There's quite a bit of code in my model view files about logic things. How do I extract it?" |
Provides refactoring workflow with decision trees for where logic belongs |
| "Should I use MVVM, TCA, or Apple's vanilla patterns?" |
Decision criteria based on app complexity, team size, testability needs |
| "How do I make my SwiftUI code testable?" |
Shows separation patterns that enable testing without SwiftUI imports |
| "Where should formatters and calculations go?" |
Anti-patterns section prevents logic in view bodies |
| "Which property wrapper do I use?" |
Decision tree for @State, @Environment, @Bindable, or plain properties |
Quick Architecture Decision Tree
What's driving your architecture choice?
โ
โโ Starting fresh, small/medium app, want Apple's patterns?
โ โโ Use Apple's Native Patterns (Part 1)
โ - @Observable models for business logic
โ - State-as-Bridge for async boundaries
โ - Property wrapper decision tree
โ
โโ Familiar with MVVM from UIKit?
โ โโ Use MVVM Pattern (Part 2)
โ - ViewModels as presentation adapters
โ - Clear View/ViewModel/Model separation
โ - Works well with @Observable
โ
โโ Complex app, need rigorous testability, team consistency?
โ โโ Consider TCA (Part 3)
โ - State/Action/Reducer/Store architecture
โ - Excellent testing story
โ - Learning curve + boilerplate trade-off
โ
โโ Complex navigation, deep linking, multiple entry points?
โโ Add Coordinator Pattern (Part 4)
- Can combine with any of the above
- Extracts navigation logic from views
- NavigationPath + Coordinator objects
Part 1: Apple's Native Patterns (iOS 26+)
Core Principle
"A data model provides separation between the data and the views that interact with the data. This separation promotes modularity, improves testability, and helps make it easier to reason about how the app works."
โ Apple Developer Documentation
Apple's modern SwiftUI patterns (WWDC 2023-2025) center on:
- @Observable for data models (replaces ObservableObject)
- State-as-Bridge for async boundaries (WWDC 2025)
- Three property wrappers: @State, @Environment, @Bindable
- Synchronous UI updates for animations
The State-as-Bridge Pattern
Problem
Async functions create suspension points that can break animations:
struct ColorExtractorView: View {
@State private var isLoading = false
var body: some View {
Button("Extract Colors") {
Task {
isLoading = true
await extractColors()
isLoading = false
}
}
.scaleEffect(isLoading ? 1.5 : 1.0)
}
}
Solution: Use State as a Bridge
"Find the boundaries between UI code that requires time-sensitive changes, and long-running async logic."
@Observable
class ColorExtractor {
var isLoading = false
var colors: [Color] = []
func extract(from image: UIImage) async {
let extracted = await heavyComputation(image)
self.colors = extracted
}
}
struct ColorExtractorView: View {
let extractor: ColorExtractor
var body: some View {
Button("Extract Colors") {
withAnimation {
extractor.isLoading = true
}
Task {
await extractor.extract(from: currentImage)
withAnimation {
extractor.isLoading = false
}
}
}
.scaleEffect(extractor.isLoading ? 1.5 : 1.0)
}
}
Benefits:
- UI logic stays synchronous (animations work correctly)
- Async code lives in the model (testable without SwiftUI)
- Clear boundary between time-sensitive UI and long-running work
Property Wrapper Decision Tree
There are only 3 questions to answer:
Which property wrapper should I use?
โ
โโ Does this model need to be STATE OF THE VIEW ITSELF?
โ โโ YES โ Use @State
โ Examples: Form inputs, local toggles, sheet presentations
โ Lifetime: Managed by the view's lifetime
โ
โโ Does this model need to be part of the GLOBAL ENVIRONMENT?
โ โโ YES โ Use @Environment
โ Examples: User account, app settings, dependency injection
โ Lifetime: Lives at app/scene level
โ
โโ Does this model JUST NEED BINDINGS?
โ โโ YES โ Use @Bindable
โ Examples: Editing a model passed from parent
โ Lightweight: Only enables $ syntax for bindings
โ
โโ NONE OF THE ABOVE?
โโ Use as plain property
Examples: Immutable data, parent-owned models
No wrapper needed: @Observable handles observation
Examples
struct DonutEditor: View {
@State private var donutToAdd = Donut()
var body: some View {
TextField("Name", text: $donutToAdd.name)
}
}
struct MenuView: View {
@Environment(Account.self) private var account
var body: some View {
Text("Welcome, \(account.userName)")
}
}
struct DonutRow: View {
@Bindable var donut: Donut
var body: some View {
TextField("Name", text: $donut.name)
}
}
struct DonutRow: View {
let donut: Donut
var body: some View {
Text(donut.name)
}
}
@Observable Model Pattern
Use @Observable for business logic that needs to trigger UI updates:
@Observable
class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
var orderCount: Int {
orders.count
}
func addDonut() {
donuts.append(Donut())
}
}
struct DonutMenu: View {
let model: FoodTruckModel
var body: some