SwiftUI Sheet, Navigation & Inspector Patterns Reference
SwiftUI Sheet, Navigation & Inspector Patterns Reference
Section titled “SwiftUI Sheet, Navigation & Inspector Patterns Reference”Table of Contents
Section titled “Table of Contents”- Sheet Patterns
- Navigation Patterns
- Multi-Column Navigation with NavigationSplitView
- Inspector
- Presentation Modifiers
- Summary Checklist
Sheet Patterns
Section titled “Sheet Patterns”Item-Driven Sheets (Preferred)
Section titled “Item-Driven Sheets (Preferred)”Use .sheet(item:) instead of .sheet(isPresented:) when presenting model-based content.
// Good - item-driven@State private var selectedItem: Item?
var body: some View { List(items) { item in Button(item.name) { selectedItem = item } } .sheet(item: $selectedItem) { item in ItemDetailSheet(item: item) }}
// Avoid - boolean flag requires separate state@State private var showSheet = false@State private var selectedItem: Item?
var body: some View { List(items) { item in Button(item.name) { selectedItem = item showSheet = true } } .sheet(isPresented: $showSheet) { if let selectedItem { ItemDetailSheet(item: selectedItem) } }}Why: .sheet(item:) automatically handles presentation state and avoids optional unwrapping in the sheet body.
Sheets Own Their Actions
Section titled “Sheets Own Their Actions”Sheets should handle their own dismiss and actions internally using @Environment(\.dismiss). Avoid passing onSave/onCancel closures from the parent – it creates callback prop-drilling and reduces reusability.
struct EditItemSheet: View { @Environment(\.dismiss) private var dismiss let item: Item @State private var name: String
init(item: Item) { self.item = item _name = State(initialValue: item.name) }
var body: some View { NavigationStack { Form { TextField("Name", text: $name) } .navigationTitle("Edit Item") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { /* save and dismiss */ } } } } }}Enum-Based Sheet Management
Section titled “Enum-Based Sheet Management”When presenting multiple different sheets, use an Identifiable enum with .sheet(item:) instead of multiple boolean state properties:
struct ArticlesView: View { enum Sheet: Identifiable { case add, edit(Article), categories var id: String { switch self { case .add: "add" case .edit(let a): "edit-\(a.id)" case .categories: "categories" } } }
@State private var presentedSheet: Sheet?
var body: some View { List { /* ... */ } .toolbar { Button("Add") { presentedSheet = .add } } .sheet(item: $presentedSheet) { sheet in switch sheet { case .add: AddArticleView() case .edit(let article): EditArticleView(article: article) case .categories: CategoriesView() } } }}Why: A single @State property and one .sheet(item:) modifier replaces N boolean properties and N sheet modifiers, improving readability and preventing only-one-sheet-at-a-time conflicts.
Navigation Patterns
Section titled “Navigation Patterns”Type-Safe Navigation with NavigationStack
Section titled “Type-Safe Navigation with NavigationStack”struct ContentView: View { var body: some View { NavigationStack { List { NavigationLink("Profile", value: Route.profile) NavigationLink("Settings", value: Route.settings) } .navigationDestination(for: Route.self) { route in switch route { case .profile: ProfileView() case .settings: SettingsView() } } } }}
enum Route: Hashable { case profile case settings}Programmatic Navigation
Section titled “Programmatic Navigation”struct ContentView: View { @State private var navigationPath = NavigationPath()
var body: some View { NavigationStack(path: $navigationPath) { List { Button("Go to Detail") { navigationPath.append(DetailRoute.item(id: 1)) } } .navigationDestination(for: DetailRoute.self) { route in switch route { case .item(let id): ItemDetailView(id: id) } } } }}
enum DetailRoute: Hashable { case item(id: Int)}Multi-Column Navigation with NavigationSplitView
Section titled “Multi-Column Navigation with NavigationSplitView”Two-Column Layout
Section titled “Two-Column Layout”Use NavigationSplitView for sidebar-driven navigation. Available on iOS 16+, macOS 13+, tvOS 16+, watchOS 9+.
struct ContentView: View { @State private var selectedItem: Item.ID?
var body: some View { NavigationSplitView { List(items, selection: $selectedItem) { item in Text(item.name) } .navigationTitle("Items") } detail: { if let selectedItem, let item = items.first(where: { $0.id == selectedItem }) { ItemDetailView(item: item) } else { ContentUnavailableView("Select an Item", systemImage: "doc") } } }}Three-Column Layout
Section titled “Three-Column Layout”struct ContentView: View { @State private var departmentId: Department.ID? @State private var employeeIds = Set<Employee.ID>()
var body: some View { NavigationSplitView { List(model.departments, selection: $departmentId) { dept in Text(dept.name) } } content: { if let department = model.department(id: departmentId) { List(department.employees, selection: $employeeIds) { emp in Text(emp.name) } } else { Text("Select a department") } } detail: { EmployeeDetails(for: employeeIds) } }}Configuration
Section titled “Configuration”- Column visibility:
NavigationSplitView(columnVisibility: $visibility)withNavigationSplitViewVisibility(.detailOnly,.doubleColumn,.all) - Column widths:
.navigationSplitViewColumnWidth(min:ideal:max:)on each column - Compact column:
NavigationSplitView(preferredCompactColumn: $column)to control which column shows on narrow devices - Style:
.navigationSplitViewStyle(.balanced)or.prominentDetail(default)
Platform Behavior
Section titled “Platform Behavior”| Platform | Behavior |
|---|---|
| macOS | Columns always visible side-by-side; sidebar has translucent material; variable-width column resizing by dragging |
| iPadOS (regular) | Sidebar can overlay or push detail; supports column visibility toggle via toolbar button |
| iOS / iPadOS (compact) | Collapses into a single NavigationStack; sidebar items show disclosure chevrons; back button navigates between columns |
| iPhone (all sizes) | Always collapsed into a stack; sidebar appears as the root list; selections push detail onto the stack |
| watchOS / tvOS | Collapses into a single stack |
Inspector
Section titled “Inspector”Availability: iOS 17.0+, macOS 14.0+
A trailing-edge panel for supplementary information.
On wider size classes (macOS, iPad landscape), it appears as a trailing column. On compact size classes (iPhone), it adapts to a sheet automatically.
Basic Inspector
Section titled “Basic Inspector”struct ShapeEditor: View { @State private var showInspector = false
var body: some View { MyEditorView() .inspector(isPresented: $showInspector) { InspectorContent() } .toolbar { ToolbarItem { Button { showInspector.toggle() } label: { Label("Inspector", systemImage: "info.circle") } } } }}Inspector with Column Width
Section titled “Inspector with Column Width”MyEditorView() .inspector(isPresented: $showInspector) { InspectorContent() .inspectorColumnWidth(min: 200, ideal: 250, max: 400) }Inspector with Fixed Width
Section titled “Inspector with Fixed Width”MyEditorView() .inspector(isPresented: $showInspector) { InspectorContent() .inspectorColumnWidth(300) }Platform Behavior
Section titled “Platform Behavior”| Platform | Behavior |
|---|---|
| macOS | Trailing-edge sidebar panel; resizable by dragging edge; integrates with window toolbar |
| iPadOS (regular) | Trailing column alongside content; toggleable via toolbar button |
| iOS / iPadOS (compact) | Adapts to a sheet presentation; swipe-to-dismiss supported |
| iPhone (all sizes) | Always presented as a sheet (no trailing column); dismiss via swipe or button |
Tip: Use
InspectorCommandsin your app’s.commandsto include the default inspector toggle keyboard shortcut.
Presentation Modifiers
Section titled “Presentation Modifiers”Full Screen Cover
Section titled “Full Screen Cover”struct ContentView: View { @State private var showFullScreen = false
var body: some View { Button("Show Full Screen") { showFullScreen = true } .fullScreenCover(isPresented: $showFullScreen) { FullScreenView() } }}Popover
Section titled “Popover”struct ContentView: View { @State private var showPopover = false
var body: some View { Button("Show Popover") { showPopover = true } .popover(isPresented: $showPopover) { PopoverContentView() .presentationCompactAdaptation(.popover) // Don't adapt to sheet on iPhone } }}For alert and confirmationDialog API patterns, see latest-apis.md.
Summary Checklist
Section titled “Summary Checklist”- Use
.sheet(item:)for model-based sheets - Sheets own their actions and dismiss internally
- Use
NavigationStackwithnavigationDestination(for:)for type-safe navigation - Use
NavigationPathfor programmatic navigation - Use
NavigationSplitViewfor sidebar-driven multi-column layouts - Use
Inspectorfor trailing-edge supplementary panels - Set column widths with
navigationSplitViewColumnWidth(min:ideal:max:)orinspectorColumnWidth(min:ideal:max:) - Use appropriate presentation modifiers (sheet, fullScreenCover, popover)
- Alerts and confirmation dialogs use modern API with actions
- Avoid passing dismiss/save callbacks to sheets
- Use enum-based
Identifiabletype with.sheet(item:)when presenting multiple sheets - Navigation state can be saved/restored when needed