# Swiftui Best Practices

> Apply SwiftUI best practices: predictable state, small views, testable logic, and correct navigation patterns.

- Skill: `rsaccone/swiftui-best-practices` (Agent Skill)
- Install (CLI): `npx skillmds@latest add rsaccone/swiftui-best-practices`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rsaccone/swiftui-best-practices/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: rsaccone (https://skillmd.com/u/rsaccone)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/rsaccone/swiftui-best-practices

---


# SwiftUI Best Practices

Follow these rules when authoring SwiftUI. Prefer modern patterns unless constrained by OS targets.

## View File Organization
- Keep feature-level SwiftUI views in their own source files.
- Do not define feature screens, sheets, or reusable subviews inside an unrelated parent view file.
- Small private helper views may stay in the parent view file only when they are tightly coupled to that parent and not useful elsewhere.
- View models should not be declared in SwiftUI view files. Put them in separate files under the feature’s `ViewModels` folder.
- Prefer feature folders that group related SwiftUI files together, for example:
  - `Timeline/Settings/Views/SettingsView.swift`
  - `Timeline/Settings/ViewModels/SettingsViewModel.swift`

## State & data flow
- Keep SwiftUI views **stateless where possible**.
- Put business logic in a ViewModel or model layer; views bind to **simple state**.
- Prefer one clear source of truth; avoid duplicating state (e.g., `@State` mirroring model fields) unless it’s an intentional edit buffer.
- Avoid passing bindings deep through many layers; consider a small view model, environment objects, or feature state.

## Observation bindings

- For `@Observable` reference models owned by a view with `@State`, use a local `@Bindable` wrapper inside `body` when passing bindings to SwiftUI controls or presentation modifiers:

  ```swift
  var body: some View {
      @Bindable var viewModel = viewModel

      content
          .sheet(isPresented: $viewModel.isPresented) {
              SheetView()
          }
  }
  ```

- Prefer `@Bindable` over manual `Binding(get:set:)` when the binding maps directly to a mutable view-model property.

- Use manual `Binding(get:set:)` only when setting the value must invoke custom logic that cannot be represented by a property setter.

- If a computed view-model property needs binding behavior, make it settable and route the setter through the existing intent method:

  ```swift
  var isPresented: Bool {
      get { coordinator.isPresented }
      set { onPresentationChange(isPresented: newValue) }
  }
  ```

- Avoid reintroducing `ObservableObject`, `@Published`, `@StateObject`, or `@ObservedObject` just to recover `$model.property` syntax. Use `@Observable`, `@State`, and `@Bindable` for new SwiftUI code unless maintaining legacy code.

## Alerts and presentation state
- Model multiple mutually-exclusive alerts with a single enum-backed state, not a growing set of `show...Alert` booleans.
- Use associated values to carry item-specific context for the alert.
- Prefer one derived alert/template surface per feature instead of many alert-template properties.
- Prefer one `.alert(...)` modifier per feature/view model when alerts are mutually exclusive.
- Repeated booleans for alerts or confirmation dialogs are usually a sign that the presentation state should be refactored into an enum.
- If an alert needs dependencies only available in the view layer, keep the alert state in the view model and pass the dependency into a single alert-builder method or expose a compact action model for the view to adapt once.

## View composition
- If a `body` grows large, extract subviews:
  - Use a **computed var** only for small, local snippets (roughly < 15 lines).
  - Use **ViewBuilder** tagged helper methods if data needs to be supplied to create the subview
  - Use a **private nested View struct** for substantial UI chunks.
- Keep modifiers close to the view they affect; avoid huge modifier chains by extracting view builders.
- Avoid deprecated SwiftUI APIs when a supported replacement exists for the project’s deployment targets.
- When replacing deprecated SwiftUI APIs, preserve existing behavior unless the task explicitly calls for a behavioral change.

## Identity & lists
- Use stable identity in lists: `ForEach(items, id: \.id)` or `Identifiable`.
- Avoid using indices as IDs unless the list is truly static.
- Don’t generate new UUIDs in `body` for identity.

## Navigation
- Prefer `NavigationStack` (and `NavigationSplitView` on iPad/macOS) over legacy APIs.
- Avoid pushing navigation state into many views; keep navigation decisions close to the feature boundary.
- Prefer typed navigation (routes as enums / Hashable) rather than stringly-typed paths.

## Performance pitfalls
- Keep `body` pure; do not start async work directly in `body`.
- Use `.task(id:)` for async work that depends on changing inputs.
- Avoid heavy work in computed properties that run during rendering.
- Use `@State` for local UI state; avoid `@ObservedObject` unless required by legacy patterns.

## Animations
- Prefer explicit animations near the state change (`withAnimation { ... }`) rather than global `.animation(...)` modifiers.
- Use `.transaction` sparingly, with intent.

## Accessibility
- Provide meaningful labels for icons and custom controls.
- Ensure tappable areas are sufficient (contentShape / padding where needed).
- Don’t rely on color alone to convey meaning.

## Output format (when generating SwiftUI code)
- Provide complete, compiling SwiftUI code.
- If introducing a ViewModel, keep it minimal and test-friendly.
- Add brief comments only where behavior is non-obvious.

