npx skills add ...
npx skills add openai/plugins --skill swiftui-view-refactor
npx skills add openai/plugins --skill swiftui-view-refactor
Refactor SwiftUI view files into stable, testable structure. Use when splitting large views, tightening data flow, or cleaning Observation ownership.
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.
private/public let@State / other stored propertiesvar (non-view)initbody@State, @Environment, @Query, .task, .task(id:), and onChange before reaching for a view model.@Environment; keep domain logic in services/models, not in the view body.some View helpersbody properties that are longer than roughly one screen or contain multiple logical sections.View types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview.some View helpers rare and small. Do not build an entire screen out of private var header: some View-style fragments.Prefer:
Avoid:
body.task, .onAppear, .onChange, or .refreshable.body or computed views that return completely different root branches via if/else.overlay, opacity, disabled, toolbar, etc.).Prefer:
Avoid:
init, then create the view model in the view's init.bootstrapIfNeeded patterns and other delayed setup workarounds.Example (Observation-based):
@Observable reference types on iOS 17+, store them as @State in the owning view.@StateObject at the owner and @ObservedObject when injecting legacy observable models.body; move business logic into services/models and keep only thin orchestration in the view.some View helpers.if-based branch swapping; move conditions to localized sections/modifiers.@State view model initialized in init.@State for root @Observable models on iOS 17+, legacy wrappers only when the deployment target requires them.some View properties.body and non-view computed vars above init.references/mv-patterns.md.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.
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)
}
}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)
}var body: some View {
List {
documentsListContent
}
.toolbar {
if canEdit {
editToolbar
}
}
}var documentsListView: some View {
if canEdit {
editableDocumentsList
} else {
readOnlyDocumentsList
}
}@State private var viewModel: SomeViewModel
init(dependency: Dependency) {
_viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}