Loading...
Loading...
Use when building SwiftUI views, managing state with @Observable, implementing NavigationStack or NavigationSplitView navigation patterns, composing view hierarchies, presenting sheets, wiring TabView, applying SwiftUI best practices, or structuring an MV-pattern app. Covers view architecture, state management, navigation, view composition, layout, List, Form, Grid, theming, environment, deep links, async loading, and performance.
npx skill4agent add dpearson2699/swift-ios-skills swiftui-patterns@State@Environment@Query.task.onChange@Environmentstruct 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)
}
}
}references/mv-patterns.md@Observable@MainActor| Wrapper | When to Use |
|---|---|
| View owns the object or value. Creates and manages lifecycle. |
| View receives an |
| View receives an |
| Access shared |
| View-local simple state: toggles, counters, text field values. Always |
| Two-way connection to parent's |
// @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)
}
}itemsisLoadingisLoadingObservableObject@StateObject@State@ObservedObjectlet@EnvironmentObject@Environment(Type.self)@Environmentprivatepubliclet@Statevarinitbodyvar body: some View {
VStack {
HeaderSection(title: title, isPinned: isPinned)
DetailsSection(details: details)
ActionsSection(onSave: onSave, onCancel: onCancel)
}
}Viewvar 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
private func statusBadge(for status: Status) -> some View {
switch status {
case .active: Text("Active").foregroundStyle(.green)
case .inactive: Text("Inactive").foregroundStyle(.secondary)
}
}ViewModifierstruct 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()) } }overlayopacitydisabledtoolbar// MARK: -struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
.navigationTitle("Items")
}
}
}path.append(item) // Push
path.removeLast() // Pop one
path = NavigationPath() // Pop to rootstruct MasterDetailView: View {
@State private var selectedItem: Item?
var body: some View {
NavigationSplitView {
List(items, selection: $selectedItem) { item in
NavigationLink(value: item) { ItemRow(item: item) }
}
.navigationTitle("Items")
} detail: {
if let item = selectedItem {
ItemDetailView(item: item)
} else {
ContentUnavailableView("Select an Item", systemImage: "sidebar.leading")
}
}
}
}.sheet(item:).sheet(isPresented:)dismiss()@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}.presentationSizing.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
.presentationSizing(.form) // .form, .page, .fitted, .automatic
}.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
.dismissalConfirmationDialog("Discard changes?", shouldPresent: hasUnsavedChanges) {
Button("Discard", role: .destructive) { discardChanges() }
}
}references/sheets.mdreferences/app-wiring.mdTabTabSectionstruct MainTabView: View {
@State private var selectedTab: AppTab = .home
var body: some View {
TabView(selection: $selectedTab) {
Tab("Home", systemImage: "house", value: .home) {
NavigationStack { HomeView() }
}
Tab("Search", systemImage: "magnifyingglass", value: .search) {
NavigationStack { SearchView() }
}
Tab("Profile", systemImage: "person", value: .profile) {
NavigationStack { ProfileView() }
}
Tab(role: .search) {
SearchView()
}
}
.tabBarMinimizeBehavior(.onScrollDown) // iOS 26: auto-hide tab bar on scroll
}
}Tab(role: .search).tabBarMinimizeBehavior(_:).onScrollDown.onScrollUp.neverTabSection.tabViewSidebarHeader/Footer/BottomBarTabViewBottomAccessoryPlacementreferences/tabview.mdreferences/app-wiring.mdprivate 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@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.taskstruct 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() }
}
}.task(id:).task(id: searchText) {
guard !searchText.isEmpty else { return }
await search(query: searchText)
}TaskonAppearTask {}ScrollView {
content
}
.scrollEdgeEffectStyle(.soft, for: .top) // iOS 26: fading edge effect
.backgroundExtensionEffect() // iOS 26: mirror/blur at safe area edgesAnimatableData@Animatable
struct PulseEffect: ViewModifier {
var scale: Double
// scale is automatically animatable -- no manual AnimatableData needed
}TextEditorAttributedStringFindContextLazyVStackLazyHStackLazyVGridLazyHGridListForEachIdentifiablebodyEquatablereferences/components-index.mdColor.primary.secondaryColor(uiColor: .systemBackground).title.headline.body.captionContentUnavailableViewhorizontalSizeClass.accessibilityLabelreferences/hig-patterns.md@ObservedObject@StateObject@Statebody.taskTaskonAppearForEach@Bindable$property@Observable@Bindable@State@ObservableNavigationViewNavigationStack.sheet(isPresented:).sheet(item:)@ObservableObservableObject@Statelet@BindableNavigationStackNavigationView.taskLazyVStackLazyHStackIdentifiablebodyViewModifier.sheet(item:).sheet(isPresented:)dismiss()@Observable@MainActorSendable