mcpField
U
ui-patterns

ui-patterns

github.com/patrickserranoDevelopment1366 installsUpdated 41w ago
Repository

About ui-patterns

A SwiftUI UI-pattern skill for developers building or extending apps. It guides view composition, state ownership, and navigation, emphasizing SwiftUI-native data flow and sheets that manage their own actions.

Key features

  1. 01Plan view state and ownership
  2. 02Compose small, focused views
  3. 03Use SwiftUI state and environment injection
  4. 04Handle asynchronous loading and errors
  5. 05Follow existing project conventions

Use cases

  1. 01Build list, detail, or editor interfaces for a feature
  2. 02Set up tabs and navigation in a new project
  3. 03Apply consistent sheet and state-management patterns

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/patrickserrano/skills --skill ui-patterns

After installing, type/ui-patternsin Claude Code to confirm it loaded.

Details

From the project README

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

Comments

Markdown supported