agentleFS
LWaetzigLWaetzig/claude-code-skills/skills/swiftui-ui-patterns/SKILL.md

ui-patterns

Skill0 starsChanged 7 months agoMITView on GitHub

Best practices and patterns for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture, composing screens, or needing component-specific guidance.

5 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. 0 are common, 7 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 7

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Add minimal AppTab enum and RouterPath

    line 18
  2. Very specific:

    Use modern SwiftUI state (@State, @Binding, @Observable, @Environment)

    line 23
  3. Distinctive:

    Avoid unnecessary view models - prefer MV (Model-View) over MVVM

    line 24
  4. Distinctive:

    Prefer composition; keep views small and focused

    line 25
  5. Very specific:

    Use async/await with .task and explicit loading/error states

    line 26
  6. Distinctive:

    Follow the project's formatter and style guide

    line 27
  7. Very specific:

    Prefer .sheet(item:) over .sheet(isPresented:) when state represents a selected model

    line 30
  8. Very specific:

    Avoid if let inside a sheet body

    line 31
  9. Distinctive:

    Add accessibility labels or identifiers for interactive UI

    line 40
  10. Distinctive:

    Keep views as pure expressions of state

    line 182
  11. Very specific:

    Put business logic in services/models injected via @Environment

    line 183
  12. Distinctive:

    Test services and models, not views

    line 184
  13. Distinctive:

    Use SwiftUI Previews for visual regression testing

    line 185

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

SwiftUI UI Patterns

Quick Start

Existing Project

  1. Identify the feature and primary interaction model (list, detail, editor, settings, tabbed)
  2. Find nearby examples in the repo with rg "TabView\(" or similar
  3. Apply local conventions: prefer SwiftUI-native state, keep state local
  4. Build with small, focused subviews and SwiftUI-native data flow

New Project Scaffolding

  1. Wire TabView + NavigationStack + sheets
  2. Add minimal AppTab enum and RouterPath
  3. Expand route and sheet enums as new screens are added

General Rules

  • Use modern SwiftUI state (@State, @Binding, @Observable, @Environment)
  • Avoid unnecessary view models - prefer MV (Model-View) over MVVM
  • Prefer composition; keep views small and focused
  • Use async/await with .task and explicit loading/error states
  • Follow the project's formatter and style guide

Sheets Best Practices

  • Prefer .sheet(item:) over .sheet(isPresented:) when state represents a selected model
  • Avoid if let inside a sheet body
  • Sheets should own their actions and call dismiss() internally

Workflow for a New SwiftUI View

  1. Define the view's state and its ownership location
  2. Identify dependencies to inject via @Environment
  3. Sketch the view hierarchy and extract repeated parts into subviews
  4. Implement async loading with .task and explicit state enum if needed
  5. Add accessibility labels or identifiers for interactive UI
  6. Validate with a build and update usage callsites if needed

MV Pattern (Preferred over MVVM)

SwiftUI views should be lightweight state expressions. Avoid ViewModels unless truly necessary.

struct FeedView: View {
    @Environment(APIClient.self) private var client

    enum ViewState {
        case loading
        case error(String)
        case loaded([Post])
    }

    @State private var viewState: ViewState = .loading

    var body: some View {
        NavigationStack {
            List {
                switch viewState {
                case .loading:
                    ProgressView("Loading...")
                case .error(let message):
                    ErrorView(message: message, retry: { await loadFeed() })
                case .loaded(let posts):
                    ForEach(posts) { post in
                        PostRow(post: post)
                    }
                }
            }
            .task { await loadFeed() }
        }
    }

    private func loadFeed() async {
        do {
            let posts = try await client.getFeed()
            viewState = .loaded(posts)
        } catch {
            viewState = .error(error.localizedDescription)
        }
    }
}

Sheet Patterns

Item-driven sheet (preferred)

@State private var selectedItem: Item?

.sheet(item: $selectedItem) { item in
    EditItemSheet(item: item)
}

Sheet owns its actions

struct EditItemSheet: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(Store.self) private var store

    let item: Item
    @State private var isSaving = false

    var body: some View {
        VStack {
            Button(isSaving ? "Saving..." : "Save") {
                Task { await save() }
            }
        }
    }

    private func save() async {
        isSaving = true
        await store.save(item)
        dismiss()
    }
}

App-Level Environment Setup

@main
struct MyApp: App {
    @State var client: APIClient = .init()
    @State var router: AppRouter = .init()

    var body: some Scene {
        WindowGroup {
            TabView(selection: $router.selectedTab) {
                ForEach(AppTab.allCases) { tab in
                    tab.rootView
                        .tag(tab)
                }
            }
            .environment(client)
            .environment(router)
        }
    }
}

State Management Guidelines

WrapperUse Case
@StateLocal, ephemeral view state
@BindingTwo-way data flow from parent
@ObservableShared state across views (iOS 17+)
@EnvironmentDependency injection, app-wide concerns
@QuerySwiftData queries directly in views

Task and onChange Patterns

// React to state changes
.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    await search(query: searchText)
}

// Respond to state transitions
.onChange(of: isActive, initial: false) {
    guard isActive else { return }
    Task { await refresh() }
}

Why Not MVVM?

SwiftUI was designed without ViewModels in mind:

  • Views are structs, lightweight and disposable
  • @State, @Environment, @Observable handle all data flow needs
  • ViewModels add complexity, indirection, and cognitive overhead
  • SwiftData's @Query works directly in views

Instead:

  • Keep views as pure expressions of state
  • Put business logic in services/models injected via @Environment
  • Test services and models, not views
  • Use SwiftUI Previews for visual regression testing

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.