Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
76
95%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Medium
Suggest reviewing before use
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.
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.
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:
@State, @Environment, @Query, .task, and .onChange for orchestration@Environment; keep views small and composablestruct 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.
@Observable Ownership RulesImportant: 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.
| 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. |
// 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.
Only use if supporting iOS 16 or earlier. @StateObject → @State, @ObservedObject → let, @EnvironmentObject → @Environment(Type.self).
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
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)
}
}Keep computed some View properties for small, stateless fragments. Extract a section into a dedicated View type when it has any of these signals:
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.
var body: some View {
List {
header
filters
results
}
}
private var header: some View {
VStack(alignment: .leading) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}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)
}
}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()) } }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.
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 refreshFeedFor iOS 17-compatible code or older compatibility shims, use manual EnvironmentKey types instead.
@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 modelContextAlways 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.
Use swift-concurrency for cancellation handlers, debounce and clocks, AsyncSequence, or actor isolation.
.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 stringsKeep 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.
LazyVStack, LazyHStack, LazyVGrid, LazyHGrid for large collections. Regular stacks render all children immediately.List/ForEach must conform to Identifiable with stable IDs. Never use array indices.body.Equatable.Follow Apple Human Interface Guidelines for layout, typography, color, and accessibility. Key rules:
Color.primary, .secondary, Color(uiColor: .systemBackground)) for automatic light/dark mode.title, .headline, .body, .caption) for Dynamic Type supportContentUnavailableView for empty and error statesspacing: on stacks unless a specific value is required — nil (the default) uses platform-appropriate adaptive spacinghorizontalSizeClass.accessibilityLabel) and support Dynamic Type accessibility sizes by switching layout orientationSee references/design-polish.md for HIG, theming, haptics, focus, transitions, and loading patterns.
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 | Reduced overlay-panel experience | 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.
@ObservedObject to create objects -- use @StateObject (legacy) or @State (modern)body -- move to model or computed property.task for async work -- manual Task in onAppear leaks if not cancelledForEach IDs -- causes incorrect diffing and UI bugs@Bindable -- $property syntax on @Observable requires @Bindable@State -- only for view-local state; shared state belongs in @ObservableView types; extensions and // MARK: only organizeNavigationView -- deprecated; use NavigationStackforegroundColor(_:) when foregroundStyle(_:) better matches semantic styling.sheet(isPresented:) when state represents a model -- use .sheet(item:) insteadAnyView 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@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"
}
}spacing: on every stack -- omit it to get adaptive platform spacing; only specify when the value is intentional.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.#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.@Observable used for shared state models (not ObservableObject on iOS 17+)@State owns objects; let/@Bindable receives themNavigationStack used (not NavigationView).task modifier for async data loadingLazyVStack/LazyHStack for large collectionsIdentifiable IDs (not array indices)// MARK: only organize filesbodyforegroundStyle(_:) used when semantic styling is preferable to a fixed colorViewModifier for repeated styling.sheet(item:) preferred over .sheet(isPresented:)dismiss() internally@Observable stores and view models are @MainActor-isolatedSendablespacing: omitted unless a specific value is required (prefer adaptive default).tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references