Agent skill
swiftui-patterns
Build SwiftUI views with modern MV architecture, state management, and view composition patterns. Covers @Observable ownership rules, @State/@Bindable/@Environment wiring, view decomposition, custom ViewModifiers, environment values, async data loading with .task, iOS 26+ APIs, Writing Tools, and performance guidelines. Use when structuring a SwiftUI app, managing state with @Observable, composing view hierarchies, or applying SwiftUI best practices.
Install this agent skill to your Project
npx add-skill https://github.com/dpearson2699/swift-ios-skills/tree/main/skills/swiftui-patterns
SKILL.md
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 and layout patterns live in dedicated sibling skills. Patterns are backward-compatible to iOS 17 unless noted.
Contents
- Architecture: Model-View (MV) Pattern
- 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.
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
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: Always annotate @Observable view model classes with @MainActor to ensure UI-bound state is updated on the main thread. Required for Swift 6 concurrency safety.
| Wrapper | When to Use |
|---|---|
@State |
View owns the object or value. Creates and manages lifecycle. |
let |
View receives an @Observable object. Read-only observation -- no wrapper needed. |
@Bindable |
View 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. |
@Binding |
Two-way connection to parent's @State or @Bindable property. |
Ownership Pattern
// @Observable view model -- always @MainActor
@MainActor
@Observable final class ItemStore {
var title = ""
var items: [Item] = []
}
// View that OWNS the model
struct ParentView: View {
@State 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) 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, @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.
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, spacing: 6) {
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(RoundedRectangle(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
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .default
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// Usage
.environment(\.theme, customTheme)
@Environment(\.theme) var theme
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@Animatablemacro -- synthesizesAnimatableDataconformance automatically (seeswiftui-animationskill)TextEditor-- now acceptsAttributedStringfor rich text
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 - 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(_:).
| Level | Effect | When to use |
|---|---|---|
.complete |
Full inline rewriting (proofread, rewrite, transform) | Notes, email, documents |
.limited |
Overlay panel only — original text untouched | Code editors, validated forms |
.disabled |
Writing Tools hidden entirely | Passwords, search bars |
.automatic |
System 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.
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 - Not extracting subviews -- long body blocks are hard to read and optimize
- Using
NavigationView-- deprecated; useNavigationStack - Inline closures in body -- extract complex closures to methods
.sheet(isPresented:)when state represents a model -- use.sheet(item:)instead- Using
AnyViewfor type erasure -- causes identity resets and disables diffing. Use@ViewBuilder,Group, or generics instead. See references/deprecated-migration.md
Review Checklist
-
@Observableused for shared state models (notObservableObjecton iOS 17+) -
@Stateowns objects;let/@Bindablereceives them -
NavigationStackused (notNavigationView) -
.taskmodifier for async data loading -
LazyVStack/LazyHStackfor large collections - Stable
IdentifiableIDs (not array indices) - Views decomposed into focused subviews
- No heavy computation in view
body - Environment used for deeply shared state
- 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
-
@Observableview model classes are@MainActor-isolated - Model types passed across concurrency boundaries are
Sendable
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, media, menus, macOS settings): references/platform-and-sharing.md
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
weatherkit
Fetch current, hourly, and daily weather forecasts and display required attribution using WeatherKit. Use when integrating weather data, showing forecasts, handling weather alerts, displaying Apple Weather attribution, or querying historical weather statistics in iOS apps.
homekit
Control smart-home accessories and commission Matter devices using HomeKit and MatterSupport. Use when managing homes/rooms/accessories, creating action sets or triggers, reading accessory characteristics, onboarding Matter devices, or building a third-party smart-home ecosystem app.
shareplay-activities
Build shared real-time experiences using GroupActivities and SharePlay. Use when implementing shared media playback, collaborative app features, synchronized game state, or any FaceTime/iMessage-integrated group activity on iOS, macOS, tvOS, or visionOS.
swiftui-gestures
Implement, review, or improve SwiftUI gesture handling. Use when adding tap, long press, drag, magnify, or rotate gestures, composing gestures with simultaneously/sequenced/exclusively, managing transient state with @GestureState, resolving parent/child gesture conflicts with highPriorityGesture or simultaneousGesture, building custom Gesture protocol conformances, or migrating from deprecated MagnificationGesture to MagnifyGesture or using the newer RotateGesture.
cryptotokenkit
Access security tokens and smart cards using CryptoTokenKit. Use when building token driver extensions with TKTokenDriver and TKToken, communicating with smart cards via TKSmartCard, implementing certificate-based authentication, managing token sessions, or integrating hardware security tokens with the system keychain.
swift-testing
Write and migrate tests using the Swift Testing framework with @Test, @Suite, #expect, #require, confirmation, parameterized tests, test tags, traits, withKnownIssue, XCTest UI testing, XCUITest, test plan, mocking, test doubles, testable architecture, snapshot testing, async test patterns, test organization, and test-driven development in Swift. Use when writing or migrating tests with Swift Testing framework, implementing parameterized tests, working with test traits, converting XCTest to Swift Testing, or setting up test organization and mocking patterns.
Didn't find tool you were looking for?