Standard: SwiftUI Standards
Rules for SwiftUI UI code. Complements coding_standards.md. Enforced in review by the SwiftUI Expert and Accessibility Expert.
Views
- The
Viewis a function of state — no networking, persistence, or business logic inbody. - Extract a subview when
bodyexceeds ~40 lines or repeats. - One primary
Viewper file plus its tightly-coupled subviews. - Side effects via
.task {}/.onChange, delegated to the ViewModel.
swift
// ✅ thin view bound to state
var body: some View {
switch viewModel.state {
case .loading: ProgressView()
case .loaded(let items): List(items) { ItemRow($0) }
case .failed(let msg): ErrorView(message: msg) { Task { await viewModel.load() } }
case .idle: Color.clear.task { await viewModel.load() }
}
}State Management
- One source of truth per piece of state; don't duplicate model data into local
@State. - New code uses Observation (
@ObservableViewModels,@Stateto own them). @Statefor view-local value state;@Bindingfor two-way child state;@Environmentfor dependency/context.- Model screen state as a single enum (
idle/loading/loaded/failed), not scattered booleans. - ViewModels are
@MainActor.
Navigation
- Use
NavigationStackwith value-based destinations (navigationDestination(for:)). - Centralize routing (router/coordinator); don't scatter navigation booleans across views.
Adaptive Layout (iPhone Duo)
Every screen must resize, because iPhone Duo runs the same app at compact width on the outer display and regular width on the inner display, folded or split. Full guidance: skills/ui/ios/iphone_duo_adaptive_layout.md.
- Adapt to
horizontalSizeClass/verticalSizeClassand container geometry. Never size to a specific device. - Prefer
NavigationSplitView,TabView,NavigationStack, andArrangementViewover custom containers. Keep navigation outside anArrangementView. - Declare bars with
.toolbar {}inside a navigation container so they can move to the vertical bar. Give every non-text item a title and a symbol (Label/Button(_:systemImage:)). - Order toolbar items Back/Close →
.topBarPinnedTrailingDone → grouped actions. UseToolbarItemGrouprather than spacers,.visibilityPriority(_:)for overflow order, andToolbarOverflowMenufor rare actions. - Custom layouts avoid active
.division/.occlusionregions fromGeometryProxy.reservedRegions(kind:). - Use
.toolbarVerticalBehavior(.disabled)only for full-screen, non-scrolling UI, and never toggle it from view state.
swift
// ✅ adaptive toolbar item
ToolbarItem(placement: .bottomBar) {
Button("New Note", systemImage: "square.and.pencil") { viewModel.newNote() }
}
.visibilityPriority(.high)Styling
- Use semantic colors and
Fonttext styles; no hardcoded font sizes (Dynamic Type). - Centralize design tokens (spacing, colors, typography) in a design system.
- Support light/dark mode; verify both.
Performance
LazyVStack/Listfor large collections; stableidinForEach.- Keep expensive work out of
body; memoize derived values. - Avoid unnecessary
AnyView; prefer@ViewBuilder.
Accessibility (required, not optional)
- Every interactive element has a label + correct trait.
- Layout survives the largest Dynamic Type size.
- Color is never the only signal; touch targets ≥ 44×44 pt.
- Add accessibility identifiers for UI testing. (Full gate:
checklists/accessibility_review.md.)
Previews
- Provide previews with injected stub dependencies and multiple states (loading/error/loaded), plus a large-Dynamic-Type variant for key screens.
- Key screens also get compact-width (iPhone Duo outer display) and regular-width (inner display) previews.