Project Context
- SwiftUI views: !
rg -l 'var body.*some View' -g '*.swift' . 2>/dev/null | head -20 || echo "none found"
SwiftUI View Refactor
Lifecycle Position
Phase 5 (Review). After build is green. Pair with code-analyzer for
architectural review and ios-testing when the refactor changes behavior. Use
XcodeRefreshCodeIssuesInFile after the refactor, and use RenderPreview or
BuildProject when you need the smallest Xcode-backed proof that the view
still compiles cleanly.
Overview
Apply a consistent structure and dependency pattern to SwiftUI views, with a focus on ordering, Model-View (MV) patterns, careful view model handling, and correct Observation usage.
Core Guidelines
1) View ordering (top → bottom)
- Environment
private/public let
@State / other stored properties
- computed
var (non-view)
init
body
- computed view builders / other view helpers
- helper / async functions
2) Prefer MV (Model-View) patterns
- Default to MV: Views are lightweight state expressions; models/services own business logic.
- Favor
@State, @Environment, @Query, and task/onChange for orchestration.
- Inject services and shared models via
@Environment; keep views small and composable.
- Split large views into subviews rather than introducing a view model.
3) Split large bodies and view properties
- If
body grows beyond a screen or has multiple logical sections, split it into smaller subviews.
- Extract large computed view properties (
var header: some View { ... }) into dedicated View types when they carry state or complex branching.
- It's fine to keep related subviews as computed view properties in the same file; extract to a standalone
View struct only when it structurally makes sense or when reuse is intended.
- Prefer passing small inputs (data, bindings, callbacks) over reusing the entire parent view state.
Example (extracting a section):
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HeaderSection(title: title, isPinned: isPinned)
DetailsSection(details: details)
ActionsSection(onSave: onSave, onCancel: onCancel)
}
}
Example (long body → shorter body + computed views in the same file):
var body: some View {
List {
header
filters
results
footer
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}
private var filters: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(filterOptions, id: \.self) { option in
FilterChip(option: option, isSelected: option == selectedFilter)
.onTapGesture { selectedFilter = option }
}
}
}
}
Example (extracting a complex computed view):
private var header: some View {
HeaderSection(title: title, subtitle: subtitle, status: status)
}
private struct HeaderSection: View {
let title: String
let subtitle: String?
let status: Status
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline)
if let subtitle { Text(subtitle).font(.subheadline) }
StatusBadge(status: status)
}
}
}
3b) Keep a stable view tree (avoid top-level conditional view swapping)
- Avoid patterns where a computed view (or
body) returns completely different root branches using if/else.
- Prefer a single stable base view, and place conditions inside sections/modifiers (
overlay, opacity, disabled, toolbar, row content, etc.).
- Root-level branch swapping can cause identity churn, broader invalidation, and extra recomputation in SwiftUI.
Prefer:
var body: some View {
List {
documentsListContent
}
.toolbar {
if canEdit {
editToolbar
}
}
}
Avoid:
var documentsListView: some View {
if canEdit {
editableDocumentsList
} else {
readOnlyDocumentsList
}
}
4) View model handling (only if already present)
- Do not introduce a view model unless the request or existing code clearly calls for one.
- If a view model exists, make it non-optional when possible.
- Pass dependencies to the view via
init, then pass them into the view model in the view's init.
- Avoid
bootstrapIfNeeded patterns.
Example (Observation-based):
@State private var viewModel: SomeViewModel
init(dependency: Dependency) {
_viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}
5) Observation usage
- For
@Observable reference types, store them as @State in the root view.
- Pass observables down explicitly as needed; avoid optional state unless required.
Workflow
- Reorder the view to match the ordering rules.
- Favor MV: move lightweight orchestration into the view using
@State, @Environment, @Query, task, and onChange.
- Ensure stable view structure: avoid top-level
if-based branch swapping; move conditions to localized sections/modifiers.
- If a view model exists, replace optional view models with a non-optional
@State view model initialized in init by passing dependencies from the view.
- Confirm Observation usage:
@State for root @Observable view models, no redundant wrappers.
- Keep behavior intact: do not change layout or business logic unless requested.
Notes
- Prefer small, explicit helpers over large conditional blocks.
- Keep computed view builders below
body and non-view computed vars above init.
- For MV-first guidance and rationale, see
references/mv-patterns.md.
Large-view handling
- When a SwiftUI view file exceeds ~300 lines, split it using extensions to group related helpers. Move async functions and helper functions into dedicated
private extensions, separated with // MARK: - comments that describe their purpose (e.g., // MARK: - Actions, // MARK: - Subviews, // MARK: - Helpers). Keep the main struct focused on stored properties, init, and body, with view-building computed vars also grouped via marks when the file is long.
1---2name: swiftui-view-refactor-23description: Refactor and review SwiftUI view files for consistent structure, dependency injection, and Observation usage. Use when asked to clean up a SwiftUI view’s layout/ordering, handle view models safely (non-optional when possible), or standardize how dependencies and @Observable state are initialized and passed.4---56## Project Context78- SwiftUI views: !`rg -l 'var body.*some View' -g '*.swift' . 2>/dev/null | head -20 || echo "none found"`910# SwiftUI View Refactor1112## Lifecycle Position1314Phase 5 (Review). After build is green. Pair with `code-analyzer` for15architectural review and `ios-testing` when the refactor changes behavior. Use16`XcodeRefreshCodeIssuesInFile` after the refactor, and use `RenderPreview` or17`BuildProject` when you need the smallest Xcode-backed proof that the view18still compiles cleanly.1920## Overview21Apply a consistent structure and dependency pattern to SwiftUI views, with a focus on ordering, Model-View (MV) patterns, careful view model handling, and correct Observation usage.2223## Core Guidelines2425### 1) View ordering (top → bottom)26- Environment27- `private`/`public` `let`28- `@State` / other stored properties29- computed `var` (non-view)30- `init`31- `body`32- computed view builders / other view helpers33- helper / async functions3435### 2) Prefer MV (Model-View) patterns36- Default to MV: Views are lightweight state expressions; models/services own business logic.37- Favor `@State`, `@Environment`, `@Query`, and `task`/`onChange` for orchestration.38- Inject services and shared models via `@Environment`; keep views small and composable.39- Split large views into subviews rather than introducing a view model.4041### 3) Split large bodies and view properties42- If `body` grows beyond a screen or has multiple logical sections, split it into smaller subviews.43- Extract large computed view properties (`var header: some View { ... }`) into dedicated `View` types when they carry state or complex branching.44- It's fine to keep related subviews as computed view properties in the same file; extract to a standalone `View` struct only when it structurally makes sense or when reuse is intended.45- Prefer passing small inputs (data, bindings, callbacks) over reusing the entire parent view state.4647Example (extracting a section):4849```swift50var body: some View {51 VStack(alignment: .leading, spacing: 16) {52 HeaderSection(title: title, isPinned: isPinned)53 DetailsSection(details: details)54 ActionsSection(onSave: onSave, onCancel: onCancel)55 }56}57```5859Example (long body → shorter body + computed views in the same file):6061```swift62var body: some View {63 List {64 header65 filters66 results67 footer68 }69}7071private var header: some View {72 VStack(alignment: .leading, spacing: 6) {73 Text(title).font(.title2)74 Text(subtitle).font(.subheadline)75 }76}7778private var filters: some View {79 ScrollView(.horizontal, showsIndicators: false) {80 HStack {81 ForEach(filterOptions, id: \.self) { option in82 FilterChip(option: option, isSelected: option == selectedFilter)83 .onTapGesture { selectedFilter = option }84 }85 }86 }87}88```8990Example (extracting a complex computed view):9192```swift93private var header: some View {94 HeaderSection(title: title, subtitle: subtitle, status: status)95}9697private struct HeaderSection: View {98 let title: String99 let subtitle: String?100 let status: Status101102 var body: some View {103 VStack(alignment: .leading, spacing: 4) {104 Text(title).font(.headline)105 if let subtitle { Text(subtitle).font(.subheadline) }106 StatusBadge(status: status)107 }108 }109}110```111112### 3b) Keep a stable view tree (avoid top-level conditional view swapping)113- Avoid patterns where a computed view (or `body`) returns completely different root branches using `if/else`.114- Prefer a single stable base view, and place conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, row content, etc.).115- Root-level branch swapping can cause identity churn, broader invalidation, and extra recomputation in SwiftUI.116117Prefer:118119```swift120var body: some View {121 List {122 documentsListContent123 }124 .toolbar {125 if canEdit {126 editToolbar127 }128 }129}130```131132Avoid:133134```swift135var documentsListView: some View {136 if canEdit {137 editableDocumentsList138 } else {139 readOnlyDocumentsList140 }141}142```143144### 4) View model handling (only if already present)145- Do not introduce a view model unless the request or existing code clearly calls for one.146- If a view model exists, make it non-optional when possible.147- Pass dependencies to the view via `init`, then pass them into the view model in the view's `init`.148- Avoid `bootstrapIfNeeded` patterns.149150Example (Observation-based):151152```swift153@State private var viewModel: SomeViewModel154155init(dependency: Dependency) {156 _viewModel = State(initialValue: SomeViewModel(dependency: dependency))157}158```159160### 5) Observation usage161- For `@Observable` reference types, store them as `@State` in the root view.162- Pass observables down explicitly as needed; avoid optional state unless required.163164## Workflow1651661) Reorder the view to match the ordering rules.1672) Favor MV: move lightweight orchestration into the view using `@State`, `@Environment`, `@Query`, `task`, and `onChange`.1683) Ensure stable view structure: avoid top-level `if`-based branch swapping; move conditions to localized sections/modifiers.1694) If a view model exists, replace optional view models with a non-optional `@State` view model initialized in `init` by passing dependencies from the view.1705) Confirm Observation usage: `@State` for root `@Observable` view models, no redundant wrappers.1716) Keep behavior intact: do not change layout or business logic unless requested.172173## Notes174175- Prefer small, explicit helpers over large conditional blocks.176- Keep computed view builders below `body` and non-view computed vars above `init`.177- For MV-first guidance and rationale, see `references/mv-patterns.md`.178179## Large-view handling180181- When a SwiftUI view file exceeds ~300 lines, split it using extensions to group related helpers. Move async functions and helper functions into dedicated `private` extensions, separated with `// MARK: -` comments that describe their purpose (e.g., `// MARK: - Actions`, `// MARK: - Subviews`, `// MARK: - Helpers`). Keep the main `struct` focused on stored properties, init, and `body`, with view-building computed vars also grouped via marks when the file is long.