SwiftUI UI Patterns
Quick start
Choose a track based on your goal:
Existing project
-
Identify the feature or screen and the primary interaction model (list, detail, editor, settings, tabbed).
-
Find a nearby example in the repo with rg "TabView(" or similar, then read the closest SwiftUI view.
-
Apply local conventions: prefer SwiftUI-native state, keep state local when possible, and use environment injection for shared dependencies.
-
Choose the relevant component reference from references/components-index.md and follow its guidance.
-
Build the view with small, focused subviews and SwiftUI-native data flow.
New project scaffolding
-
Start with references/app-scaffolding-wiring.md to wire TabView + NavigationStack + sheets.
-
Add a minimal AppTab and RouterPath based on the provided skeletons.
-
Choose the next component reference based on the UI you need first (TabView, NavigationStack, Sheets).
-
Expand the route and sheet enums as new screens are added.
General rules to follow
-
Use modern SwiftUI state (@State , @Binding , @Observable , @Environment ) and avoid unnecessary view models.
-
Prefer composition; keep views small and focused.
-
Use async/await with .task and explicit loading/error states.
-
Maintain existing legacy patterns only when editing legacy files.
-
Follow the project's formatter and style guide.
-
Sheets: Prefer .sheet(item:) over .sheet(isPresented:) when state represents a selected model. Avoid if let inside a sheet body. Sheets should own their actions and call dismiss() internally instead of forwarding onCancel /onConfirm closures.
Workflow for a new SwiftUI view
-
Define the view's state and its ownership location.
-
Identify dependencies to inject via @Environment .
-
Sketch the view hierarchy and extract repeated parts into subviews.
-
Implement async loading with .task and explicit state enum if needed.
-
Add accessibility labels or identifiers when the UI is interactive.
-
Validate with a build and update usage callsites if needed.
Component references
Use references/components-index.md as the entry point. Each component reference should include:
-
Intent and best-fit scenarios.
-
Minimal usage pattern with local conventions.
-
Pitfalls and performance notes.
-
Paths to existing examples in the current repo.
Sheet patterns
Item-driven sheet (preferred)
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in EditItemSheet(item: item) }
Sheet owns its actions
struct EditItemSheet: View { @Environment(.dismiss) private var dismiss @Environment(Store.self) private var store
let item: Item
@State private var isSaving = false
var body: some View {
VStack {
Button(isSaving ? "Saving…" : "Save") {
Task { await save() }
}
}
}
private func save() async {
isSaving = true
await store.save(item)
dismiss()
}
}
Adding a new component reference
-
Create references/<component>.md .
-
Keep it short and actionable; link to concrete files in the current repo.
-
Update references/components-index.md with the new entry.