SwiftUI Patterns
Modern SwiftUI patterns for building declarative, performant user interfaces on Apple platforms. Covers the Observation framework, view composition, type-safe navigation, and performance optimization.
When to Activate
- Building SwiftUI views and managing state (
@State,@Observable,@Binding) - Designing navigation flows with
NavigationStack - Structuring view models and data flow
- Optimizing rendering performance for lists and complex layouts
- Working with environment values and dependency injection in SwiftUI
State Management
Property Wrapper Selection
Choose the simplest wrapper that fits:
@Observable ViewModel
Use @Observable (not ObservableObject) — it tracks property-level changes so SwiftUI only re-renders views that read the changed property:
View Consuming the ViewModel
Environment Injection
Replace @EnvironmentObject with @Environment:
View Composition
Extract Subviews to Limit Invalidation
Break views into small, focused structs. When state changes, only the subview reading that state re-renders:
ViewModifier for Reusable Styling
Navigation
Type-Safe NavigationStack
Use NavigationStack with NavigationPath for programmatic, type-safe routing:
Performance
Use Lazy Containers for Large Collections
LazyVStack and LazyHStack create views only when visible:
Stable Identifiers
Always use stable, unique IDs in ForEach — avoid using array indices:
Avoid Expensive Work in body
- Never perform I/O, network calls, or heavy computation inside
body - Use
.task {}for async work — it cancels automatically when the view disappears - Use
.sensoryFeedback()and.geometryGroup()sparingly in scroll views - Minimize
.shadow(),.blur(), and.mask()in lists — they trigger offscreen rendering
Equatable Conformance
For views with expensive bodies, conform to Equatable to skip unnecessary re-renders:
Previews
Use #Preview macro with inline mock data for fast iteration:
Anti-Patterns to Avoid
- Using
ObservableObject/@Published/@StateObject/@EnvironmentObjectin new code — migrate to@Observable - Putting async work directly in
bodyorinit— use.task {}or explicit load methods - Creating view models as
@Stateinside child views that don't own the data — pass from parent instead - Using
AnyViewtype erasure — prefer@ViewBuilderorGroupfor conditional views - Ignoring
Sendablerequirements when passing data to/from actors
References
See skill: swift-actor-persistence for actor-based persistence patterns.
See skill: swift-protocol-di-testing for protocol-based DI and testing with Swift Testing.


