Guide Skill — This is an expert workflow/pattern guide, not API reference documentation.
Originally from Dimillian/Skills by Thomas Ricouard. MIT License.
SwiftUI View Refactor
Overview
Refactor SwiftUI views toward small, explicit, stable view types. Default to vanilla SwiftUI: local state in the view, shared dependencies in the environment, business logic in services/models, and view models only when the request or existing code clearly requires one.
Core Guidelines
1) View ordering (top → bottom)
- Enforce this ordering unless the existing file has a stronger local convention you must preserve.
- Environment
private/public let
@State / other stored properties
- computed
var (non-view)
init
body
- computed view builders / other view helpers
- helper / async functions
2) Default to MV, not MVVM
- Views should be lightweight state expressions and orchestration points, not containers for business logic.
- Favor
@State, @Environment, @Query, .task, .task(id:), and onChange before reaching for a view model.
- Inject services and shared models via
@Environment; keep domain logic in services/models, not in the view body.
- Do not introduce a view model just to mirror local view state or wrap environment dependencies.
- If a screen is getting large, split the UI into subviews before inventing a new view model layer.
3) Strongly prefer dedicated subview types over computed some View helpers
- Flag
body properties that are longer than roughly one screen or contain multiple logical sections.
- Prefer extracting dedicated
View types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview.
- Keep computed
some View helpers rare and small. Do not build an entire screen out of private var header: some View-style fragments.
- Pass small, explicit inputs (data, bindings, callbacks) into extracted subviews instead of handing down the entire parent state.
- If an extracted subview becomes reusable or independently meaningful, move it to its own file.
Prefer:
var body: some View {
List {
HeaderSection(title: title, subtitle: subtitle)
FilterSection(
filterOptions: filterOptions,
selectedFilter: $selectedFilter
)
ResultsSection(items: filteredItems)
FooterSection()
}
}
private struct HeaderSection: View {
let title: String
let subtitle: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}
}
private struct FilterSection: View {
let filterOptions: [FilterOption]
@Binding var selectedFilter: FilterOption
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(filterOptions, id: \.self) { option in
FilterChip(option: option, isSelected: option == selectedFilter)
.onTapGesture { selectedFilter = option }
}
}
}
}
}
Avoid:
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)
}
}
3b) Extract actions and side effects out of body
- Do not keep non-trivial button actions inline in the view body.
- Do not bury business logic inside
.task, .onAppear, .onChange, or .refreshable.
- Prefer calling small private methods from the view, and move real business logic into services/models.
- The body should read like UI, not like a view controller.
Button("Save", action: save)
.disabled(isSaving)
.task(id: searchText) {
await reload(for: searchText)
}
private func save() {
Task { await saveAsync() }
}
private func reload(for searchText: String) async {
guard !searchText.isEmpty else {
results = []
return
}
await searchService.search(searchText)
}
4) Keep a stable view tree (avoid top-level conditional view swapping)
- Avoid
body or computed views that return completely different root branches via if/else.
- Prefer a single stable base view with conditions inside sections/modifiers (
overlay, opacity, disabled, toolbar, etc.).
- Root-level branch swapping causes identity churn, broader invalidation, and extra recomputation.
Prefer:
var body: some View {
List {
documentsListContent
}
.toolbar {
if canEdit {
editToolbar
}
}
}
Avoid:
var documentsListView: some View {
if canEdit {
editableDocumentsList
} else {
readOnlyDocumentsList
}
}
5) View model handling (only if already present or explicitly requested)
- Treat view models as a legacy or explicit-need pattern, not the default.
- 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 create the view model in the view's init.
- Avoid
bootstrapIfNeeded patterns and other delayed setup workarounds.
Example (Observation-based):
@State private var viewModel: SomeViewModel
init(dependency: Dependency) {
_viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}
6) Observation usage
- For
@Observable reference types on iOS 17+, store them as @State in the owning view.
- Pass observables down explicitly; avoid optional state unless the UI genuinely needs it.
- If the deployment target includes iOS 16 or earlier, use
@StateObject at the owner and @ObservedObject when injecting legacy observable models.
Workflow
- Reorder the view to match the ordering rules.
- Remove inline actions and side effects from
body; move business logic into services/models and keep only thin orchestration in the view.
- Shorten long bodies by extracting dedicated subview types; avoid rebuilding the screen out of many computed
some View helpers.
- Ensure stable view structure: avoid top-level
if-based branch swapping; move conditions to localized sections/modifiers.
- If a view model exists or is explicitly required, replace optional view models with a non-optional
@State view model initialized in init.
- Confirm Observation usage:
@State for root @Observable models on iOS 17+, legacy wrappers only when the deployment target requires them.
- Keep behavior intact: do not change layout or business logic unless requested.
Notes
- Prefer small, explicit view types over large conditional blocks and large computed
some View properties.
- Keep computed view builders below
body and non-view computed vars above init.
- A good SwiftUI refactor should make the view read top-to-bottom as data flow plus layout, not as mixed layout and imperative logic.
- For MV-first guidance and rationale, see
references/mv-patterns.md.
Large-view handling
When a SwiftUI view file exceeds ~300 lines, split it aggressively. Extract meaningful sections into dedicated View types instead of hiding complexity in many computed properties. Use private extensions with // MARK: - comments for actions and helpers, but do not treat extensions as a substitute for breaking a giant screen into smaller view types. If an extracted subview is reused or independently meaningful, move it into its own file.
1---2name: guide-swiftui-view-refactor3description: Refactor and review SwiftUI view files with strong defaults for small dedicated subviews, MV-over-MVVM data flow, stable view trees, explicit dependency injection, and correct Observation usage. Use when cleaning up a SwiftUI view, splitting long bodies, removing inline actions or side effects, reducing computed `some View` helpers, or standardizing `@Observable` and view model initialization patterns.4license: MIT5---6
7> **Guide Skill** — This is an expert workflow/pattern guide, not API reference documentation.
8> Originally from [Dimillian/Skills](https://github.com/Dimillian/Skills) by Thomas Ricouard. MIT License.
9
10
11# SwiftUI View Refactor
12
13## Overview
14Refactor SwiftUI views toward small, explicit, stable view types. Default to vanilla SwiftUI: local state in the view, shared dependencies in the environment, business logic in services/models, and view models only when the request or existing code clearly requires one.
15
16## Core Guidelines
17
18### 1) View ordering (top → bottom)
19- Enforce this ordering unless the existing file has a stronger local convention you must preserve.
20- Environment
21- `private`/`public` `let`
22- `@State` / other stored properties
23- computed `var` (non-view)
24- `init`
25- `body`
26- computed view builders / other view helpers
27- helper / async functions
28
29### 2) Default to MV, not MVVM
30- Views should be lightweight state expressions and orchestration points, not containers for business logic.
31- Favor `@State`, `@Environment`, `@Query`, `.task`, `.task(id:)`, and `onChange` before reaching for a view model.
32- Inject services and shared models via `@Environment`; keep domain logic in services/models, not in the view body.
33- Do not introduce a view model just to mirror local view state or wrap environment dependencies.
34- If a screen is getting large, split the UI into subviews before inventing a new view model layer.
35
36### 3) Strongly prefer dedicated subview types over computed `some View` helpers
37- Flag `body` properties that are longer than roughly one screen or contain multiple logical sections.
38- Prefer extracting dedicated `View` types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview.
39- Keep computed `some View` helpers rare and small. Do not build an entire screen out of `private var header: some View`-style fragments.
40- Pass small, explicit inputs (data, bindings, callbacks) into extracted subviews instead of handing down the entire parent state.
41- If an extracted subview becomes reusable or independently meaningful, move it to its own file.
42
43Prefer:
44
45```swift
46var body: some View {
47 List {
48 HeaderSection(title: title, subtitle: subtitle)
49 FilterSection(
50 filterOptions: filterOptions,
51 selectedFilter: $selectedFilter
52 )
53 ResultsSection(items: filteredItems)
54 FooterSection()
55 }
56}
57
58private struct HeaderSection: View {
59 let title: String
60 let subtitle: String
61
62 var body: some View {
63 VStack(alignment: .leading, spacing: 6) {
64 Text(title).font(.title2)
65 Text(subtitle).font(.subheadline)
66 }
67 }
68}
69
70private struct FilterSection: View {
71 let filterOptions: [FilterOption]
72 @Binding var selectedFilter: FilterOption
73
74 var body: some View {
75 ScrollView(.horizontal, showsIndicators: false) {
76 HStack {
77 ForEach(filterOptions, id: \.self) { option in
78 FilterChip(option: option, isSelected: option == selectedFilter)
79 .onTapGesture { selectedFilter = option }
80 }
81 }
82 }
83 }
84}
85```
86
87Avoid:
88
89```swift
90var body: some View {
91 List {
92 header
93 filters
94 results
95 footer
96 }
97}
98
99private var header: some View {
100 VStack(alignment: .leading, spacing: 6) {
101 Text(title).font(.title2)
102 Text(subtitle).font(.subheadline)
103 }
104}
105```
106
107### 3b) Extract actions and side effects out of `body`
108- Do not keep non-trivial button actions inline in the view body.
109- Do not bury business logic inside `.task`, `.onAppear`, `.onChange`, or `.refreshable`.
110- Prefer calling small private methods from the view, and move real business logic into services/models.
111- The body should read like UI, not like a view controller.
112
113```swift
114Button("Save", action: save)
115 .disabled(isSaving)
116
117.task(id: searchText) {
118 await reload(for: searchText)
119}
120
121private func save() {
122 Task { await saveAsync() }
123}
124
125private func reload(for searchText: String) async {
126 guard !searchText.isEmpty else {
127 results = []
128 return
129 }
130 await searchService.search(searchText)
131}
132```
133
134### 4) Keep a stable view tree (avoid top-level conditional view swapping)
135- Avoid `body` or computed views that return completely different root branches via `if/else`.
136- Prefer a single stable base view with conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, etc.).
137- Root-level branch swapping causes identity churn, broader invalidation, and extra recomputation.
138
139Prefer:
140
141```swift
142var body: some View {
143 List {
144 documentsListContent
145 }
146 .toolbar {
147 if canEdit {
148 editToolbar
149 }
150 }
151}
152```
153
154Avoid:
155
156```swift
157var documentsListView: some View {
158 if canEdit {
159 editableDocumentsList
160 } else {
161 readOnlyDocumentsList
162 }
163}
164```
165
166### 5) View model handling (only if already present or explicitly requested)
167- Treat view models as a legacy or explicit-need pattern, not the default.
168- Do not introduce a view model unless the request or existing code clearly calls for one.
169- If a view model exists, make it non-optional when possible.
170- Pass dependencies to the view via `init`, then create the view model in the view's `init`.
171- Avoid `bootstrapIfNeeded` patterns and other delayed setup workarounds.
172
173Example (Observation-based):
174
175```swift
176@State private var viewModel: SomeViewModel
177
178init(dependency: Dependency) {
179 _viewModel = State(initialValue: SomeViewModel(dependency: dependency))
180}
181```
182
183### 6) Observation usage
184- For `@Observable` reference types on iOS 17+, store them as `@State` in the owning view.
185- Pass observables down explicitly; avoid optional state unless the UI genuinely needs it.
186- If the deployment target includes iOS 16 or earlier, use `@StateObject` at the owner and `@ObservedObject` when injecting legacy observable models.
187
188## Workflow
189
1901. Reorder the view to match the ordering rules.
1912. Remove inline actions and side effects from `body`; move business logic into services/models and keep only thin orchestration in the view.
1923. Shorten long bodies by extracting dedicated subview types; avoid rebuilding the screen out of many computed `some View` helpers.
1934. Ensure stable view structure: avoid top-level `if`-based branch swapping; move conditions to localized sections/modifiers.
1945. If a view model exists or is explicitly required, replace optional view models with a non-optional `@State` view model initialized in `init`.
1956. Confirm Observation usage: `@State` for root `@Observable` models on iOS 17+, legacy wrappers only when the deployment target requires them.
1967. Keep behavior intact: do not change layout or business logic unless requested.
197
198## Notes
199
200- Prefer small, explicit view types over large conditional blocks and large computed `some View` properties.
201- Keep computed view builders below `body` and non-view computed vars above `init`.
202- A good SwiftUI refactor should make the view read top-to-bottom as data flow plus layout, not as mixed layout and imperative logic.
203- For MV-first guidance and rationale, see `references/mv-patterns.md`.
204
205## Large-view handling
206
207When a SwiftUI view file exceeds ~300 lines, split it aggressively. Extract meaningful sections into dedicated `View` types instead of hiding complexity in many computed properties. Use `private` extensions with `// MARK: -` comments for actions and helpers, but do not treat extensions as a substitute for breaking a giant screen into smaller view types. If an extracted subview is reused or independently meaningful, move it into its own file.