Skip to content
Back to skills

Swiftui Patterns

ASecurity

Use when building native iOS with SwiftUI - small composable views, observable state models, atomic components, Swift 6.2+ concurrency, and system styling over hardcoded values

  • 109 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 19, 2026
ai-agentsgoswiftawstestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add makifbaysal/tasktrooper --skill swiftui-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Swiftui Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Swiftui Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/makifbaysal-swiftui-patterns/badge)](https://www.skillsdirectory.com/skills/makifbaysal-swiftui-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: swiftui-patterns
category: architecture
description: Use when building native iOS with SwiftUI - small composable views, observable state models, atomic components, Swift 6.2+ concurrency, and system styling over hardcoded values
tech_stack: Swift
source: twostraws/SwiftUI-Agent-Skill (MIT), AvdLee/SwiftUI-Agent-Skill (MIT), adapted
---
# SwiftUI Patterns

## Overview

Native iOS (SwiftUI) is chosen when a task needs deep Apple-platform integration or the app is already native (see native-vs-flutter-decision). Same discipline as Flutter: small composed views, state out of the view, reusable atomic components.

**Core principle:** Views are a function of state. Data and business logic live in an observable model, not in the `View`.

## Rules

- **Small views, composed.** Extract subviews as their own `View` types (not `@ViewBuilder` funcs) so each has its own body and preview.
- **State ownership is explicit:** `@State` for local ephemeral UI only (and it is always `private`); app/business state in an `@MainActor @Observable` model injected via `init`/`@Environment`. Views send intents to the model; the model owns the logic and the networking.
- **Atomic components:** a shared component library (atoms → molecules → organisms) of reusable views. Reuse before building; grep before adding.
- **System styling over hardcoded values:** colors from the asset catalog / `Color` semantic roles, spacing/typography from a tokens file (see `mobile-design-system-foundation`) — not scattered magic numbers. Support Dynamic Type and dark mode.
- **`#Preview`** every reusable view in its key states (loading/data/error), and in the matrix: Dynamic Type `.accessibility3`, dark, `traits: .landscapeLeft`.
- **Value types** (`struct`) for models and view state; reference types only where identity is needed.
- **Never name a domain type `Task`** — it shadows `_Concurrency.Task`, which every `async`/`.task {}` call site needs; a rename (`BoardItem`, `WorkItem`, …) is a five-minute fix versus a confusing compile error later.

## Concurrency

- Follow the target's default actor isolation: a new Xcode 26 project defaults to `@MainActor` isolation, so a view model that touches UI state should be `@MainActor @Observable` explicitly rather than relying on inference holding across Swift versions.
- No GCD (`DispatchQueue.main.async`, etc.) in new code — `async`/`await`, `Task { }`, `Task.sleep(for:)`.
- Kick off async work from `.task { }` on the view, not `onAppear` + a detached `Task`.

## Worked Example

```swift
struct BoardItem: Identifiable, Sendable { let id: Int; let title: String }   // never name a model `Task`
enum LoadState<T> { case loading, empty, data(T), failed(String) }

@MainActor @Observable final class ItemListModel {
    private(set) var state: LoadState<[BoardItem]> = .loading
    private let repo: any BoardItemRepository
    init(repo: any BoardItemRepository) { self.repo = repo }
    func load() async {
        state = .loading
        do { let items = try await repo.list(); state = items.isEmpty ? .empty : .data(items) }
        catch { state = .failed(error.localizedDescription) }
    }
}

struct ItemListScreen: View {
    @State private var model: ItemListModel
    init(model: ItemListModel) { _model = State(initialValue: model) }
    var body: some View {
        Group {
            switch model.state {
            case .loading: AppSpinner()
            case .empty: ContentUnavailableView("No items yet", systemImage: "tray")
            case .failed(let message):
                ContentUnavailableView {
                    Label("Couldn't load items", systemImage: "exclamationmark.triangle")
                } description: { Text(message) } actions: {
                    Button("Try again") { Task { await model.load() } }
                }
            case .data(let items): ItemList(items: items)
            }
        }
        .task { await model.load() }
    }
}
```

Typechecks with `swiftc -swift-version 6` (verified). The original example's domain `struct Task` shadowed `_Concurrency.Task`, giving `error: missing argument for parameter 'title' in call` at every `Task { }` call site — renamed to `BoardItem` here. Note the explicit `.empty` case rendered with `ContentUnavailableView` and a retry action on `.failed`, and the `init` that seeds `State(initialValue:)` so the model can be injected.

Previews at the matrix (typechecked against the iOS 18 simulator SDK):

```swift
#Preview("Phone, AX3, dark") { ItemListScreen(model: .preview).environment(\.dynamicTypeSize, .accessibility3).preferredColorScheme(.dark) }
#Preview("Landscape", traits: .landscapeLeft) { ItemListScreen(model: .preview) }
```

For a layout that must switch at large accessibility sizes: `typeSize.isAccessibilitySize ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())`. For an icon-only control: `Button("Add to cart", systemImage: "cart.badge.plus", action:)` with `.labelStyle(.iconOnly)` and `.frame(minWidth: 44, minHeight: 44)` (see `mobile-accessibility`).

## Rules for testing

- Unit-test the `@Observable` model with a mocked repository (no UI).
- New tests in **Swift Testing** (`import Testing`, `@Test`, `#expect`) where the target already uses Xcode 16+; XCTest stays for existing UI tests and `app.performAccessibilityAudit()`. Test-first.

## Modern API

- `foregroundStyle` (not `foregroundColor`), `.clipShape(.rect(cornerRadius:))` (not `.cornerRadius`), the `Tab` API for tab bars, the two-parameter `onChange(of:) { old, new in }`, `.topBarTrailing` placement, `containerRelativeFrame` instead of `GeometryReader` when it covers the case, never `UIScreen.main.bounds`.
- `Color.withValues` is not a thing in SwiftUI (that's the Flutter equivalent) — use asset-catalog colours or `Color(.sRGB, ...)`/`.opacity()` as the repo already does.

## Liquid Glass (iOS 26+)

- Standard bars, tab views and sheets adopt Liquid Glass automatically when built with Xcode 26+. Don't paint an opaque custom background over a toolbar/tab bar to "fix" this.
- Use `.glassEffect` only on custom floating controls, and only when the task asks for it.
- `UIDesignRequiresCompatibility` is ignored when building with the iOS 27 SDK — don't rely on it to opt back out of the new look.

## Common Mistakes

- Networking or business rules inside `body` / `View`.
- `@ViewBuilder` helper funcs instead of real subviews.
- Hardcoded colors/sizes instead of asset-catalog + tokens.
- No previews, so every change needs a full build to see.
- A domain model named `Task`.
- GCD calls in new code instead of `async`/`await`.

## Red Flags

- A `View` with a URLSession call in it.
- Magic `Color(red:...)` and paddings sprinkled across views.
- A 200-line `body`.
- A type named `Task` anywhere in the diff.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…