Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dpearson2699 avatar

Swiftui Patterns

  • 3.2k installs
  • 944 repo stars
  • Updated July 15, 2026
  • dpearson2699/swift-ios-skills

swiftui-patterns is an agent skill that guides modern SwiftUI architecture with Model-View defaults, @Observable ownership rules, view composition, environment wiring, and async .task loading for iOS 26+ with iOS 17 comp

About

swiftui-patterns is a SwiftUI architecture guide for iOS developers who want maintainable screen structure without unnecessary ViewModels. The skill defaults to Model-View: views express lightweight state while models and services own business logic, and it documents when existing ViewModels must be respected versus when new ones are justified. Coverage includes app wiring, dependency graph setup, environment versus initializer injection, and lightweight HTTP or API clients that avoid bloating views. Developers reach for swiftui-patterns when SwiftUI screens accumulate logic, tests become hard to write, or MVVM layers add indirection without clear benefit. The patterns emphasize pragmatic MV over dogmatic MVVM while preserving test seams and clear ownership boundaries across features.

  • Default to Model-View (MV) pattern for SwiftUI instead of MVVM
  • Views stay lightweight using @State, @Environment, @Query and .task
  • Services and models live in the environment and are tested in isolation
  • Split large views into subviews rather than adding ViewModels
  • Clear decision rules for when a ViewModel is justified

Swiftui Patterns by the numbers

  • 3,206 all-time installs (skills.sh)
  • +139 installs in the week ending Jul 29, 2026 (Skillselion tracking)
  • Ranked #152 of 2,244 Frontend Development skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-patterns

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs3.2k
repo stars944
Security audit3 / 3 scanners passed
Last updatedJuly 15, 2026
Repositorydpearson2699/swift-ios-skills

How do you structure SwiftUI views with maintainable state ownership without unnecessary ViewModels bloating every screen?

Apply proven SwiftUI architecture patterns that keep views lightweight and business logic testable.

Who is it for?

iOS developers building SwiftUI apps who want MV architecture, granular @Observable tracking, and clear rules for when ViewModels are justified.

Skip if: Skip for UIKit-only apps, server-side Swift, or navigation or Liquid Glass topics covered by sibling swiftui-navigation and swiftui-liquid-glass skills.

When should I use this skill?

Structuring SwiftUI app state, managing @Observable ownership, composing view hierarchies, or correcting SwiftUI pattern guidance.

What you get

Lightweight composable views using @State, @Bindable, and @Environment with tested models and services, stable view trees, and .task-based async loading.

  • MV-structured views
  • dependency graph
  • lightweight service clients

By the numbers

  • Targets iOS 26+ with Swift 6.3 while noting backward compatibility to iOS 17
  • Review checklist and common mistakes section for architecture and state anti-patterns

Files

SKILL.mdMarkdownGitHub ↗

SwiftUI Patterns

Modern SwiftUI patterns targeting iOS 26+ with Swift 6.3. Covers architecture, state management, view composition, environment wiring, async loading, design polish, and platform/share integration. Navigation, layout, animation, and Liquid Glass patterns live in dedicated sibling skills. Patterns are backward-compatible to iOS 17 unless noted.

Contents

Scope boundary: This skill covers architecture, state ownership, composition, environment wiring, async loading, and related SwiftUI app structure patterns. Detailed navigation patterns are covered in the swiftui-navigation skill, including NavigationStack, NavigationSplitView, sheets, tabs, and deep-linking patterns. Detailed layout, container, and component patterns are covered in the swiftui-layout-components skill, including stacks, grids, lists, scroll view patterns, forms, controls, search UI with .searchable, overlays, and related layout components. Detailed animation choreography is covered in swiftui-animation. Liquid Glass adoption, custom glass controls, scroll edge effects, .scrollEdgeEffectStyle, and .backgroundExtensionEffect are covered in swiftui-liquid-glass.

Architecture: Model-View (MV) Pattern

Default to MV -- views are lightweight state expressions; models and services own business logic. Do not introduce view models unless the existing code already uses them.

Core principles:

  • Favor @State, @Environment, @Query, .task, and .onChange for orchestration
  • Inject services and shared models via @Environment; keep views small and composable
  • Split large views into smaller subviews rather than introducing a view model
  • Test models, services, and business logic; keep views simple and declarative
struct FeedView: View {
    @Environment(FeedClient.self) private var client

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

    @State private var viewState: ViewState = .loading

    var body: some View {
        List {
            switch viewState {
            case .loading:
                ProgressView()
            case .error(let message):
                ContentUnavailableView("Error", systemImage: "exclamationmark.triangle",
                                       description: Text(message))
            case .loaded(let posts):
                ForEach(posts) { post in
                    PostRow(post: post)
                }
            }
        }
        .task { await loadFeed() }
        .refreshable { await loadFeed() }
    }

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

For MV pattern rationale, app wiring, and lightweight client examples, see references/architecture-patterns.md.

State Management

@Observable Ownership Rules

Important: Isolate UI-bound @Observable stores and view models on @MainActor when SwiftUI views own them, mutate them, or bind to their properties. Observation tracks changes; it does not make shared mutable state thread-safe. Domain models that do not touch UI state can use their own isolation strategy.

WrapperWhen to Use
@StateView owns the object or value. Creates and manages lifecycle.
letView receives an @Observable object. Read-only observation -- no wrapper needed.
@BindableView receives an @Observable object and needs two-way bindings ($property).
@Environment(Type.self)Access shared @Observable object from environment.
@State (value types)View-local simple state: toggles, counters, text field values. Always private.
@BindingTwo-way connection to parent's @State or @Bindable property.

Ownership Pattern

// UI-bound @Observable store -- main-actor isolated
@MainActor
@Observable final class ItemStore {
    var title = ""
    var items: [Item] = []
}

// View that OWNS the model
struct ParentView: View {
    @State private var viewModel = ItemStore()

    var body: some View {
        ChildView(store: viewModel)
            .environment(viewModel)
    }
}

// View that READS (no wrapper needed for @Observable)
struct ChildView: View {
    let store: ItemStore

    var body: some View { Text(store.title) }
}

// View that BINDS (needs two-way access)
struct EditView: View {
    @Bindable var store: ItemStore

    var body: some View {
        TextField("Title", text: $store.title)
    }
}

// View that reads from ENVIRONMENT
struct DeepView: View {
    @Environment(ItemStore.self) private var store

    var body: some View {
        @Bindable var s = store
        TextField("Title", text: $s.title)
    }
}

Granular tracking: SwiftUI only re-renders views that read properties that changed. If a view reads items but not isLoading, changing isLoading does not trigger a re-render. This is a major performance advantage over ObservableObject.

Legacy ObservableObject

Only use if supporting iOS 16 or earlier. @StateObject@State, @ObservedObjectlet, @EnvironmentObject@Environment(Type.self).

View Ordering Convention

Order members top to bottom: 1) @Environment 2) let properties 3) @State / stored properties 4) computed var 5) init 6) body 7) view builders / helpers 8) async functions

View Composition

Extract Subviews

Break views into focused subviews. Each should have a single responsibility.

var body: some View {
    VStack {
        HeaderSection(title: title, isPinned: isPinned)
        DetailsSection(details: details)
        ActionsSection(onSave: onSave, onCancel: onCancel)
    }
}

Computed View Properties

Keep related subviews as computed properties in the same file; extract to a standalone View struct when reuse is intended or the subview carries its own state.

var body: some View {
    List {
        header
        filters
        results
    }
}

private var header: some View {
    VStack(alignment: .leading) {
        Text(title).font(.title2)
        Text(subtitle).font(.subheadline)
    }
}

ViewBuilder Functions

For conditional logic that does not warrant a separate struct:

@ViewBuilder
private func statusBadge(for status: Status) -> some View {
    switch status {
    case .active: Text("Active").foregroundStyle(.green)
    case .inactive: Text("Inactive").foregroundStyle(.secondary)
    }
}

Custom View Modifiers

Extract repeated styling into ViewModifier:

struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background)
            .clipShape(.rect(cornerRadius: 12))
            .shadow(radius: 2)
    }
}
extension View { func cardStyle() -> some View { modifier(CardStyle()) } }

Stable View Tree

Avoid top-level conditional view swapping. Prefer a single stable base view with conditions inside sections or modifiers. When a view file exceeds ~300 lines, split with extensions and // MARK: - comments.

Environment

Custom Environment Values

Use @Entry for custom environment values and actions. It generates the entry boilerplate for EnvironmentValues.

extension EnvironmentValues {
    @Entry var theme: Theme = .default
    @Entry var refreshFeed: @Sendable () async -> Void = {}
}

// Usage
.environment(\.theme, customTheme)
.environment(\.refreshFeed) { await feedStore.refresh() }

@Environment(\.theme) private var theme
@Environment(\.refreshFeed) private var refreshFeed

For iOS 17-compatible code or older compatibility shims, use manual EnvironmentKey types instead.

Common Built-in Environment Values

@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@Environment(\.horizontalSizeClass) var sizeClass
@Environment(\.isSearching) var isSearching
@Environment(\.openURL) var openURL
@Environment(\.modelContext) var modelContext

Async Data Loading

Always use .task -- it cancels automatically on view disappear:

struct ItemListView: View {
    @State var store = ItemStore()

    var body: some View {
        List(store.items) { item in
            ItemRow(item: item)
        }
        .task { await store.load() }
        .refreshable { await store.refresh() }
    }
}

Use .task(id:) to re-run when a dependency changes:

.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    await search(query: searchText)
}

Never create manual Task in onAppear unless you need to store a reference for cancellation. Exception: Task {} is acceptable in synchronous action closures (e.g., Button actions) for immediate state updates before async work.

iOS 26+ New APIs

  • `.scrollEdgeEffectStyle(.soft, for: .top)` -- fading edge effect on scroll edges
  • `.backgroundExtensionEffect()` -- mirror/blur at safe area edges
  • `@Animatable` macro -- synthesizes AnimatableData conformance automatically (see swiftui-animation skill)
  • `TextEditor(text: Binding<AttributedString>)` -- rich text editing with attributed strings

Keep these as routing reminders in this skill. For Liquid Glass visual treatment, scroll edge effects, glass controls, and availability gating, use swiftui-liquid-glass; for detailed animation APIs, use swiftui-animation.

Clipboard command modifiers are not iOS 26 defaults: .copyable, .cuttable, and command-based .pasteDestination(for:action:validator:) are macOS 13+ and iOS/iPadOS/Mac Catalyst 27 beta in current Apple docs. For iOS 26 targets, use UIPasteboard for custom clipboard commands, or use drag/drop and ShareLink for Transferable flows. See references/platform-and-sharing.md.

Performance Guidelines

  • Lazy stacks/grids: Use LazyVStack, LazyHStack, LazyVGrid, LazyHGrid for large collections. Regular stacks render all children immediately.
  • Stable IDs: All items in List/ForEach must conform to Identifiable with stable IDs. Never use array indices.
  • Avoid body recomputation: Move filtering and sorting to computed properties or the model, not inline in body.
  • Equatable views: For complex views that re-render unnecessarily, conform to Equatable.

HIG Alignment

Follow Apple Human Interface Guidelines for layout, typography, color, and accessibility. Key rules:

  • Use semantic colors (Color.primary, .secondary, Color(uiColor: .systemBackground)) for automatic light/dark mode
  • Use system font styles (.title, .headline, .body, .caption) for Dynamic Type support
  • Use ContentUnavailableView for empty and error states
  • Omit spacing: on stacks unless a specific value is required — nil (the default) uses platform-appropriate adaptive spacing
  • Support adaptive layouts via horizontalSizeClass
  • Provide VoiceOver labels (.accessibilityLabel) and support Dynamic Type accessibility sizes by switching layout orientation

See references/design-polish.md for HIG, theming, haptics, focus, transitions, and loading patterns.

Writing Tools (iOS 18+)

Control the Apple Intelligence Writing Tools experience on text views with .writingToolsBehavior(_:).

LevelEffectWhen to use
.completeFull inline rewriting (proofread, rewrite, transform)Notes, email, documents
.limitedReduced overlay-panel experienceCode editors, validated forms
.disabledWriting Tools hidden entirelyPasswords, search bars
.automaticSystem chooses based on context (default)Most views
TextEditor(text: $body)
    .writingToolsBehavior(.complete)
TextField("Search…", text: $query)
    .writingToolsBehavior(.disabled)

Detecting active sessions: Read isWritingToolsActive on UITextView (UIKit) to defer validation or suspend undo grouping until a rewrite finishes.

Docs: WritingToolsBehavior · writingToolsBehavior(_:))

Common Mistakes

1. Using @ObservedObject to create objects -- use @StateObject (legacy) or @State (modern) 2. Heavy computation in view body -- move to model or computed property 3. Not using .task for async work -- manual Task in onAppear leaks if not cancelled 4. Array indices as ForEach IDs -- causes incorrect diffing and UI bugs 5. Forgetting @Bindable -- $property syntax on @Observable requires @Bindable 6. Over-using @State -- only for view-local state; shared state belongs in @Observable 7. Not extracting subviews -- long body blocks are hard to read and optimize 8. Using NavigationView -- deprecated; use NavigationStack 9. Reaching for foregroundColor(_:) when foregroundStyle(_:) better matches semantic styling 10. Inline closures in body -- extract complex closures to methods 11. .sheet(isPresented:) when state represents a model -- use .sheet(item:) instead 12. Using `AnyView` for routine branching -- type erasure hides structure and can hurt performance or identity-sensitive transitions. Use @ViewBuilder, Group, or generics unless an API genuinely needs heterogeneous view storage. See references/deprecated-migration.md 13. Putting `@AppStorage` inside an `@Observable` class -- @AppStorage is a SwiftUI DynamicProperty; it only triggers view updates when used directly in a View. Inside an @Observable class, observation tracking never sees the change. Keep @AppStorage in views, or read/write UserDefaults directly inside the @Observable class:

// Wrong -- @AppStorage is invisible to @Observable tracking
@MainActor @Observable final class Settings {
    @AppStorage("theme") var theme: String = "system" // view won't update
}

// Right -- UserDefaults read/write with a normal stored property
@MainActor @Observable final class Settings {
    var theme: String {
        didSet { UserDefaults.standard.set(theme, forKey: "theme") }
    }

    init() {
        theme = UserDefaults.standard.string(forKey: "theme") ?? "system"
    }
}

14. Hard-coding spacing: on every stack -- omit it to get adaptive platform spacing; only specify when the value is intentional 15. Treating .copyable, .cuttable, or command-based .pasteDestination(for:action:validator:) as iOS 16/iOS 26 APIs -- they are macOS 13+ and iOS/iPadOS/Mac Catalyst 27 beta in current Apple docs. Use UIPasteboard, drag/drop, or ShareLink for iOS 26 targets. 16. Treating modern defaults as formal deprecations -- #Preview is the modern preview default, but PreviewProvider is legacy rather than compiler-deprecated. EditButton, .onDelete, and .onMove remain valid for edit-mode list workflows; use .swipeActions for contextual row actions.

Review Checklist

  • [ ] @Observable used for shared state models (not ObservableObject on iOS 17+)
  • [ ] @State owns objects; let/@Bindable receives them
  • [ ] Migration and availability claims checked for current platform support, especially clipboard and sharing APIs
  • [ ] NavigationStack used (not NavigationView)
  • [ ] .task modifier for async data loading
  • [ ] LazyVStack/LazyHStack for large collections
  • [ ] Stable Identifiable IDs (not array indices)
  • [ ] Views decomposed into focused subviews
  • [ ] No heavy computation in view body
  • [ ] Environment used for deeply shared state
  • [ ] foregroundStyle(_:) used when semantic styling is preferable to a fixed color
  • [ ] Custom ViewModifier for repeated styling
  • [ ] .sheet(item:) preferred over .sheet(isPresented:)
  • [ ] Sheets own their actions and call dismiss() internally
  • [ ] MV pattern followed -- no unnecessary view models
  • [ ] UI-bound @Observable stores and view models are @MainActor-isolated
  • [ ] Model types passed across concurrency boundaries are Sendable
  • [ ] Stack spacing: omitted unless a specific value is required (prefer adaptive default)

References

  • Architecture, app wiring, and lightweight clients: references/architecture-patterns.md
  • Design polish (HIG, theming, haptics, transitions, loading, focus): references/design-polish.md
  • Deprecated API migration: references/deprecated-migration.md
  • Platform and sharing patterns (Transferable, clipboard availability, media, menus, macOS settings): references/platform-and-sharing.md

Related skills

How it compares

Use swiftui-patterns when simplifying MV-heavy SwiftUI code; reach for platform API docs when the task is single-component styling only.

FAQ

Does swiftui-patterns require MVVM?

No. It defaults to Model-View where views are lightweight state expressions and models or services own business logic, adding ViewModels only when existing code already uses them or rules justify them.

How should @Observable objects be wrapped in views?

@State when the view owns the object, let for read-only observation, @Bindable for two-way bindings, and @Environment(Type.self) for shared stores.

What async loading pattern does it recommend?

Always use .task which cancels on disappear, with .refreshable for pull-to-refresh and .task(id:) when dependencies like searchText change.

Is Swiftui Patterns safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.