home / skills / steipete / agent-scripts / swiftui-view-refactor

swiftui-view-refactor skill

/skills/swiftui-view-refactor

This skill helps you refactor SwiftUI views for consistent structure, safe view models, and clear dependency injection to improve maintainability.

npx playbooks add skill steipete/agent-scripts --skill swiftui-view-refactor

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
4.7 KB
---
name: swiftui-view-refactor
description: 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.
---

# SwiftUI View Refactor

_Attribution: copied from @Dimillian’s `Dimillian/Skills` (2025-12-31)._

## 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):

```swift
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):

```swift
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):

```swift
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)
        }
    }
}
```

### 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):

```swift
@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

1) Reorder the view to match the ordering rules.
2) Favor MV: move lightweight orchestration into the view using `@State`, `@Environment`, `@Query`, `task`, and `onChange`.
3) 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.
4) Confirm Observation usage: `@State` for root `@Observable` view models, no redundant wrappers.
5) 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`.

Overview

This skill refactors and reviews SwiftUI view files to enforce a consistent structure, safe dependency injection, and correct Observation usage. It focuses on ordering, Model-View patterns, and turning optional view models into safely-initialized state where appropriate. Use it to standardize view layout, lifecycle patterns, and how observable state is passed and stored.

How this skill works

The skill inspects SwiftUI view source and reorders members to a predictable top-to-bottom layout (environment, lets, stored state, non-view computed vars, init, body, view helpers, helper functions). It checks for large bodies and extracts subviews or computed view builders, ensures services and shared models are injected via init or @Environment, and replaces optional observable view models with non-optional @State-initialized instances when dependencies allow. The review preserves existing layout and behavior unless a structural change is requested.

When to use it

  • When a SwiftUI view file has inconsistent ordering or hard-to-follow member placement.
  • When a view uses optional view models that should be safely initialized.
  • When Observation wrappers (@Observable, @State) are applied incorrectly or redundantly.
  • When body is large and needs splitting into subviews or computed view builders.
  • When dependencies should be passed explicitly (init/@Environment) instead of being bootstrapped.

Best practices

  • Keep views lightweight: move business logic to models or services and orchestrate with @State, @Environment, @Query, task, and onChange.
  • Follow the view ordering: Environment → lets → stored state → non-view computed vars → init → body → view helpers → helper functions.
  • Store reference @Observable objects as @State on the root view and pass them explicitly to children.
  • Avoid introducing a new view model unless the code clearly requires one; prefer subviews for large bodies.
  • Initialize non-optional view models in init via State(initialValue:) by passing required dependencies.

Example use cases

  • Refactor a screen where body contains multiple logical sections into a parent view with small Section subviews.
  • Convert an optional viewModel stored property into @State non-optional initialized from an injected dependency.
  • Standardize how shared services are injected via @Environment instead of global bootstrap patterns.
  • Replace redundant observation wrappers so a single correct wrapper (e.g., @State for @Observable) lives at the root view.
  • Extract complex computed view properties into private View structs when they carry state or branching logic.

FAQ

Will this change layout or behavior?

No — the skill preserves visual layout and business logic unless you explicitly request behavioral changes.

When should I introduce a view model?

Only introduce a view model if the view clearly needs non-trivial presentation logic or shared state that warrants a separate type; otherwise prefer models/services and splitting into subviews.