Skills swiftui-view-refactor
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.
git clone https://github.com/Dimillian/Skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/Dimillian/Skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/swiftui-view-refactor" ~/.claude/skills/dimillian-skills-swiftui-view-refactor && rm -rf "$T"
swiftui-view-refactor/SKILL.mdSwiftUI 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
/privatepubliclet
/ other stored properties@State- computed
(non-view)var initbody- 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
, and.task(id:)
before reaching for a view model.onChange - Inject services and shared models via
; keep domain logic in services/models, not in the view body.@Environment - 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
some View- Flag
properties that are longer than roughly one screen or contain multiple logical sections.body - Prefer extracting dedicated
types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview.View - Keep computed
helpers rare and small. Do not build an entire screen out ofsome View
-style fragments.private var header: some View - 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
body- Do not keep non-trivial button actions inline in the view body.
- Do not bury business logic inside
,.task
,.onAppear
, or.onChange
..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
or computed views that return completely different root branches viabody
.if/else - Prefer a single stable base view with conditions inside sections/modifiers (
,overlay
,opacity
,disabled
, etc.).toolbar - 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
, then create the view model in the view'sinit
.init - Avoid
patterns and other delayed setup workarounds.bootstrapIfNeeded
Example (Observation-based):
@State private var viewModel: SomeViewModel init(dependency: Dependency) { _viewModel = State(initialValue: SomeViewModel(dependency: dependency)) }
6) Observation usage
- For
reference types on iOS 17+, store them as@Observable
in the owning view.@State - Pass observables down explicitly; avoid optional state unless the UI genuinely needs it.
- If the deployment target includes iOS 16 or earlier, use
at the owner and@StateObject
when injecting legacy observable models.@ObservedObject
Workflow
- Reorder the view to match the ordering rules.
- Remove inline actions and side effects from
; move business logic into services/models and keep only thin orchestration in the view.body - Shorten long bodies by extracting dedicated subview types; avoid rebuilding the screen out of many computed
helpers.some View - Ensure stable view structure: avoid top-level
-based branch swapping; move conditions to localized sections/modifiers.if - If a view model exists or is explicitly required, replace optional view models with a non-optional
view model initialized in@State
.init - Confirm Observation usage:
for root@State
models on iOS 17+, legacy wrappers only when the deployment target requires them.@Observable - 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
properties.some View - Keep computed view builders below
and non-view computed vars abovebody
.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.