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.
// ❌ 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.
// ❌ WRONG — destination outside stack hierarchy
NavigationStack {
ContentView()
}
.navigationDestination(for: Item.self) { ... } // Never triggers!
// ✅ CORRECT — destination inside stack
NavigationStack {
ContentView()
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
Sheet vs FullScreenCover vs NavigationLink
Is it a modal workflow? (user must complete or cancel)
├─ YES
│ └─ Should user see parent context?
│ ├─ YES → .sheet (can dismiss by swiping)
│ └─ NO → .fullScreenCover (must tap button)
│
└─ NO (progressive disclosure in same flow)
└─ NavigationLink / .navigationDestination
Sheet state gotcha: Sheet content is created BEFORE presentation. @StateObject inside sheet reinitializes on each presentation.
// ❌ PROBLEM — viewModel resets each time sheet opens
.sheet(isPresented: $show) {
SheetView() // New @StateObject created each time
}
// ✅ SOLUTION — pass data or use item binding
.sheet(item: $selectedItem) { item in
SheetView(item: item) // Item drives content
}
NEVER Do
View Identity & Performance
NEVER use AnyView unless absolutely necessary:
// ❌ Destroys view identity — state lost, animations break
func makeView() -> AnyView {
if condition {
return AnyView(ViewA())
} else {
return AnyView(ViewB())
}
}
// ✅ Use @ViewBuilder — preserves identity
@ViewBuilder
func makeView() -> some View {
if condition {
ViewA()
} else {
ViewB()
}
}
NEVER compute expensive values in body:
// ❌ Recomputed on EVERY view update
var body: some View {
let processed = expensiveComputation(data) // Runs constantly
Text(processed)
}
// ✅ Use .task or computed property with caching
var body: some View {
Text(cachedResult)
.task(id: data) {
cachedResult = await expensiveComputation(data)
}
}
NEVER change view identity during animation:
// ❌ Animation breaks — different views
if isExpanded {
ExpandedCard() // One view
} else {
CompactCard() // Different view
}
// ✅ Same view, different state — smooth animation
CardView(isExpanded: isExpanded)
.animation(.spring(), value: isExpanded)
State Management
NEVER mutate @Published from background thread:
// ❌ Undefined behavior — sometimes works, sometimes crashes
Task.detached {
viewModel.items = newItems // Background thread!
}
// ✅ Always MainActor for @Published
Task { @MainActor in
viewModel.items = newItems
}
// Or mark entire ViewModel as @MainActor
NEVER use .onAppear for async data loading:
// ❌ No cancellation, runs multiple times
.onAppear {
Task { await loadData() } // Not cancelled on disappear
}
// ✅ Use .task — automatic cancellation
.task {
await loadData() // Cancelled when view disappears
}
NEVER store derived state that should be computed:
// ❌ State duplication — can become inconsistent
@State private var items: [Item] = []
@State private var itemCount: Int = 0 // Derived from items!
// ✅ Compute derived values
@State private var items: [Item] = []
var itemCount: Int { items.count }
Lists & ForEach
NEVER use array index as id:
// ❌ Bugs when array changes — wrong rows update
ForEach(items.indices, id: \.self) { index in
ItemRow(item: items[index])
}
// ✅ Use stable identifier
ForEach(items) { item in // Requires Identifiable
ItemRow(item: item)
}
// Or explicit id
ForEach(items, id: \.stableId) { item in ... }
NEVER put List inside ScrollView:
// ❌ Double scrolling, broken behavior
ScrollView {
List(items) { ... }
}
// ✅ List handles its own scrolling
List(items) { item in
ItemRow(item: item)
}
iOS/tvOS Platform Patterns
tvOS Focus System
#if os(tvOS)
struct TVCardView: View {
@Environment(\.isFocused) var isFocused
var body: some View {
VStack {
Image(item.image)
Text(item.title)
}
.scaleEffect(isFocused ? 1.1 : 1.0)
.animation(.easeInOut(duration: 0.15), value: isFocused)
// tvOS: 10ft viewing distance = larger touch targets
.frame(width: 300, height: 400)
}
}
struct TVRowView: View {
@FocusState private var focusedIndex: Int?
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 48) { // tvOS needs larger spacing
ForEach(items.indices, id: \.self) { index in
TVCardView(item: items[index])
.focusable()
.focused($focusedIndex, equals: index)
}
}
.padding(.horizontal, 90) // Safe area for overscan
}
.onAppear { focusedIndex = 0 }
}
}
#endif
tvOS gotcha: Focus system REQUIRES explicit .focusable() on custom views. Without it, remote navigation skips the view entirely.
Adaptive Layout Decision
struct AdaptiveView: View {
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
// iPhone portrait: compact, iPad/iPhone landscape: regular
if sizeClass == .compact {
VStack { content }
} else {
HStack { sidebar; content }
}
}
}
// Or use ViewThatFits for automatic selection
ViewThatFits {
HStack { wideContent } // Try first
VStack { narrowContent } // Fallback
}
Performance Patterns
Preventing Unnecessary Redraws
// ✅ Equatable conformance for diffing
struct ItemRow: View, Equatable {
let item: Item
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.item.id == rhs.item.id &&
lhs.item.name == rhs.item.name
}
var body: some View {
Text(item.name)
}
}
// ✅ Extract child views to isolate updates
struct ParentView: View {
@StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
// Only rerenders when header data changes
HeaderView(title: viewModel.title)
// Only rerenders when items change
ItemList(items: viewModel.items)
}
}
}
Lazy Loading Patterns
// ✅ LazyVStack for large lists — views created on demand
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item: item) // Created when scrolled into view
}
}
}
// ✅ task(id:) for dependent async work
.task(id: searchQuery) {
// Automatically cancels previous task when searchQuery changes
results = await search(searchQuery)
}
Common Gotchas
Sheet/Alert Binding Timing
// ❌ PROBLEM — item is nil when sheet renders
@State var selectedItem: Item?
.sheet(isPresented: Binding(
get: { selectedItem != nil },
set: { if !$0 { selectedItem = nil } }
)) {
ItemDetail(item: selectedItem!) // Crash! nil during transition
}
// ✅ SOLUTION — use item binding
.sheet(item: $selectedItem) { item in
ItemDetail(item: item) // item guaranteed non-nil
}
GeometryReader Sizing
// ❌ GeometryReader expands to fill available space
VStack {
GeometryReader { geo in
Text("Small text") // But GeometryReader takes ALL space
}
Text("Never visible") // Pushed off screen
}
// ✅ Wrap in fixed-size container or use sparingly
VStack {
Text("Visible")
Text("Also visible")
}
.background(
GeometryReader { geo in
Color.clear.onAppear { size = geo.size }
}
)
Animation + State Change Timing
// ❌ State change THEN animation — no animation occurs
showDetail = true
withAnimation { } // Nothing to animate
// ✅ State change INSIDE withAnimation
withAnimation(.spring()) {
showDetail = true // This change gets animated
}
Quick Reference
Property Wrapper Cheat Sheet
| Wrapper | Creates | Survives Update | Use Case |
|---|---|---|---|
| @State | ✅ | ✅ | View-local value types |
| @StateObject | ✅ | ✅ | View-owned ObservableObject |
| @ObservedObject | ❌ | ❌ | Parent-passed ObservableObject |
| @Binding | ❌ | N/A | Two-way value connection |
| @EnvironmentObject | ❌ | N/A | App-wide shared state |
Modifier Order Matters
// Different results!
Text("A").padding().background(.red) // Red includes padding
Text("B").background(.red).padding() // Red only behind text
Common order: content modifiers → padding → background → frame → position
More from kaakati/rails-enterprise-dev
flutter conventions & best practices
Dart 3.x and Flutter 3.x conventions, naming patterns, code organization, null safety, and async/await best practices
55getx state management patterns
GetX controllers, reactive state, dependency injection, bindings, navigation, and best practices
52tailadmin ui patterns
TailAdmin dashboard UI framework patterns and Tailwind CSS classes. ALWAYS use this skill when: (1) Building any dashboard or admin panel interface, (2) Creating data tables, cards, charts, or metrics displays, (3) Implementing forms, buttons, alerts, or modals, (4) Building navigation (sidebar, header, breadcrumbs), (5) Any UI work that should follow TailAdmin design. This skill REQUIRES fetching from the official GitHub repository to ensure accurate class usage - NEVER invent classes.
39mvvm-architecture
Expert MVVM decisions for iOS/tvOS: choosing between ViewModel patterns (state enum vs published properties vs Combine), service layer boundaries, dependency injection strategies, and testing approaches. Use when designing ViewModel architecture, debugging data flow issues, or deciding where business logic belongs. Trigger keywords: MVVM, ViewModel, ObservableObject, @StateObject, service layer, dependency injection, unit test, mock, architecture
36rails localization (i18n) - english & arabic
Comprehensive internationalization skill for Ruby on Rails applications with proper English and Arabic translations, RTL support, pluralization rules, date/time formatting, and culturally appropriate content adaptation.
34rspec testing patterns
Complete guide to testing Ruby on Rails applications with RSpec. Use this skill when writing unit tests, integration tests, system tests, or when setting up test infrastructure including factories, shared examples, and mocking strategies.
31