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
- Architecture: Model-View (MV) Pattern
- Workflow
- State Management
- View Ordering Convention
- View Composition
- Environment
- Async Data Loading
- iOS 26+ New APIs
- Performance Guidelines
- HIG Alignment
- Writing Tools (iOS 18+)
- Common Mistakes
- Review Checklist
- References
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.
Workflow
- Record the current state ownership, actions, side effects, navigation, and lifecycle behavior.
- Choose the smallest MV/state/composition change that preserves that contract.
- Build after each structural step; fix compiler and isolation errors before continuing.
- Render deterministic previews for loaded, loading, empty, and error states as applicable, including required environment dependencies.
- Exercise important interactions and side effects. If behavior changes, restore the fixture, fix the smallest boundary, and rerun the same build, preview, and interaction checks.
Load Behavior-Preserving View Refactoring [blocked] for restructuring existing views and Isolated Preview Construction [blocked] for fixture and dependency patterns.
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.onChangefor 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
For MV pattern rationale, app wiring, and lightweight client examples, see references/architecture-patterns.md [blocked].
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.
Ownership Pattern
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, @ObservedObject → let, @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. When restructuring an existing view, load Behavior-Preserving View Refactoring [blocked] for action/side-effect boundaries and build/preview proof.
Computed View Properties
Keep computed some View properties for small, stateless fragments. Extract a section into a dedicated View type when it has any of these signals:
- meaningful branching or substantial layout
- its own state or async lifecycle
- narrower Observation dependencies than the parent
- a useful independent preview
- enough complexity to obscure the parent's data flow
When narrowing dependencies, pass only the values, bindings, and actions the child needs. If they form a large but cohesive interface, pass a feature-scoped @Observable model. Observation limits invalidation to properties the child reads, but an app-wide store still creates a broad interface; reserve it for children that genuinely need that cohesive state.
Reuse is a useful outcome, not a prerequisite for decomposition.
Extensions and // MARK: - organize a large file; they do not create view boundaries or replace extraction.
ViewBuilder Functions
For conditional logic that does not warrant a separate struct:
Custom View Modifiers
Extract repeated styling into ViewModifier:
Stable View Tree
Avoid top-level conditional view swapping. Prefer a single stable base view with conditions inside sections or modifiers.
When extracted views need independent state coverage, deterministic fixtures, or environment setup, load Isolated Preview Construction [blocked].
Environment
Custom Environment Values
Use @Entry for custom environment values and actions. It generates the entry boilerplate for EnvironmentValues.
For iOS 17-compatible code or older compatibility shims, use manual EnvironmentKey types instead.
Common Built-in Environment Values
Async Data Loading
Always use .task -- it cancels automatically on view disappear:
Use .task(id:) to re-run when a dependency changes:
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.
Use swift-concurrency for cancellation handlers, debounce and clocks, AsyncSequence, or actor isolation.
iOS 26+ New APIs
Route .scrollEdgeEffectStyle, .backgroundExtensionEffect, and glass controls to swiftui-liquid-glass; route @Animatable to swiftui-animation. TextEditor(text: Binding<AttributedString>) is the iOS 26 rich-text editing path. Keep availability checks beside code that adopts these APIs.
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 [blocked].
Performance Guidelines
- Lazy stacks/grids: Use
LazyVStack,LazyHStack,LazyVGrid,LazyHGridfor large collections. Regular stacks render all children immediately. - Stable IDs: All items in
List/ForEachmust conform toIdentifiablewith 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
ContentUnavailableViewfor 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 [blocked] 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(_:).
Detecting active sessions: Read isWritingToolsActive on UITextView (UIKit) to defer validation or suspend undo grouping until a rewrite finishes.
Common Mistakes
-
Using
@ObservedObjectto create objects -- use@StateObject(legacy) or@State(modern) -
Heavy computation in view
body-- move to model or computed property -
Not using
.taskfor async work -- manualTaskinonAppearleaks if not cancelled -
Array indices as
ForEachIDs -- causes incorrect diffing and UI bugs -
Forgetting
@Bindable--$propertysyntax on@Observablerequires@Bindable -
Over-using
@State-- only for view-local state; shared state belongs in@Observable -
Keeping complex or independently previewable sections computed -- extract
Viewtypes; extensions and// MARK:only organize -
Using
NavigationView-- deprecated; useNavigationStack -
Reaching for
foregroundColor(_:)whenforegroundStyle(_:)better matches semantic styling -
Inline closures in body -- extract complex closures to methods
-
.sheet(isPresented:)when state represents a model -- use.sheet(item:)instead -
Using
AnyViewfor 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 [blocked] -
Putting
@AppStorageinside an@Observableclass.@AppStorageis a viewDynamicProperty; keep it in aView, or expose a normal observed property backed byUserDefaultsin the model. -
Hard-coding
spacing:on every stack -- omit it to get adaptive platform spacing; only specify when the value is intentional -
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. UseUIPasteboard, drag/drop, orShareLinkfor iOS 26 targets. -
Treating modern defaults as formal deprecations --
#Previewis the modern preview default, butPreviewProvideris legacy rather than compiler-deprecated.EditButton,.onDelete, and.onMoveremain valid for edit-mode list workflows; use.swipeActionsfor contextual row actions. -
Making a required dependency optional to stop a preview crash -- install deterministic preview dependencies instead, without live networking, authentication, production databases, or global singletons
Review Checklist
-
@Observableused for shared state models (notObservableObjecton iOS 17+) -
@Stateowns objects;let/@Bindablereceives them - Migration and availability claims checked for current platform support, especially clipboard and sharing APIs
-
NavigationStackused (notNavigationView) -
.taskmodifier for async data loading -
LazyVStack/LazyHStackfor large collections - Stable
IdentifiableIDs (not array indices) - Extraction uses branching/layout, lifecycle, dependency, preview, or parent-flow signals; small stateless fragments stay computed
- Extensions and
// MARK:only organize files - Structure-only refactors preserve behavior; use thin action/lifecycle methods, keep reusable logic in services/models, then build and render useful previews
- Previews cover meaningful loaded/loading/empty/error states with deterministic fixtures and every required environment dependency
- No heavy computation in view
body - Environment used for deeply shared state
-
foregroundStyle(_:)used when semantic styling is preferable to a fixed color - Custom
ViewModifierfor 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
@Observablestores 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 [blocked]
- Design polish (HIG, theming, haptics, transitions, loading, focus): references/design-polish.md [blocked]
- Deprecated API migration: references/deprecated-migration.md [blocked]
- Platform and sharing patterns (Transferable, clipboard availability, media, menus, macOS settings): references/platform-and-sharing.md [blocked]
- Isolated preview construction (state coverage, fixtures, and environment dependencies): references/preview-isolation.md [blocked]
- Existing-view restructuring (behavior contract, action/side-effect boundaries, and verification): references/view-refactoring.md [blocked]


