Expert SwiftUI decisions: property wrapper selection (@State vs @StateObject vs @ObservedObject gotchas), navigation architecture (NavigationStack vs NavigationSplitView), performance traps (body recomputation, identity), and platform-specific patterns for tvOS focus. Use when building SwiftUI views for iOS/tvOS, debugging view update issues, or choosing navigation patterns. Trigger keywords: SwiftUI, @State, @StateObject, @ObservedObject, NavigationStack, sheet, animation, tvOS focus, view identity, body
View on GitHubKaakati/rails-enterprise-dev
reactree-ios-dev
January 25, 2026
Select agents to install to:
npx add-skill https://github.com/Kaakati/rails-enterprise-dev/blob/main/plugins/reactree-ios-dev/skills/swiftui-patterns/SKILL.md -a claude-code --skill swiftui-patternsInstallation paths:
.claude/skills/swiftui-patterns/# SwiftUI Patterns — Expert Decisions
Expert decision frameworks for SwiftUI choices that require experience. Claude knows SwiftUI syntax — this skill provides the judgment calls that prevent subtle bugs.
---
## Decision Trees
### Property Wrapper Selection
```
Who creates the object?
├─ This view creates it
│ └─ Is it a value type (struct, primitive)?
│ ├─ YES → @State
│ └─ NO (class/ObservableObject)
│ └─ iOS 17+?
│ ├─ YES → @Observable class + var (no wrapper)
│ └─ NO → @StateObject
│
└─ Parent passes it down
└─ Is it an ObservableObject?
├─ YES → @ObservedObject
└─ NO
└─ Need two-way binding?
├─ YES → @Binding
└─ NO → Regular parameter
```
**The @StateObject vs @ObservedObject trap**: Using `@ObservedObject` for a locally-created object causes recreation on EVERY view update. State vanishes randomly.
```swift
// ❌ BROKEN — viewModel recreated on parent rerender
struct BadView: View {
@ObservedObject var viewModel = UserViewModel() // WRONG
}
// ✅ CORRECT — viewModel survives view updates
struct GoodView: View {
@StateObject private var viewModel = UserViewModel()
}
```
### Navigation Pattern Selection
```
How many columns needed?
├─ 1 column (stack-based)
│ └─ NavigationStack with .navigationDestination
│
├─ 2 columns (list → detail)
│ └─ NavigationSplitView (2 column)
│ └─ iPad: sidebar + detail
│ └─ iPhone: collapses to stack
│
└─ 3 columns (sidebar → list → detail)
└─ NavigationSplitView (3 column)
└─ Mail/Files app pattern
```
**NavigationStack gotcha**: `navigationDestination(for:)` must be attached to a view INSIDE the NavigationStack, not on the NavigationStack itself. Wrong placement = silent failure.
```swift
// ❌ WRONG — destination outside stack hierarchy
NavigationStack {
ContentView()
}
.navigationDestination(for: Item.self) { ... } // Never triggers!
// ✅ CORRECT — destination inside stack
NavigationStack {
ContentVie