Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/sheet-navigation-patterns.md
1# SwiftUI Sheet, Navigation & Inspector Patterns Reference23## Table of Contents45- [Sheet Patterns](#sheet-patterns)6- [Navigation Patterns](#navigation-patterns)7- [Multi-Column Navigation with NavigationSplitView](#multi-column-navigation-with-navigationsplitview)8- [Inspector](#inspector)9- [Presentation Modifiers](#presentation-modifiers)10- [Summary Checklist](#summary-checklist)1112## Sheet Patterns1314### Item-Driven Sheets (Preferred)1516**Use `.sheet(item:)` instead of `.sheet(isPresented:)` when presenting model-based content.**1718```swift19// Good - item-driven20@State private var selectedItem: Item?2122var body: some View {23List(items) { item in24Button(item.name) {25selectedItem = item26}27}28.sheet(item: $selectedItem) { item in29ItemDetailSheet(item: item)30}31}3233// Avoid - boolean flag requires separate state34@State private var showSheet = false35@State private var selectedItem: Item?3637var body: some View {38List(items) { item in39Button(item.name) {40selectedItem = item41showSheet = true42}43}44.sheet(isPresented: $showSheet) {45if let selectedItem {46ItemDetailSheet(item: selectedItem)47}48}49}50```5152**Why**: `.sheet(item:)` automatically handles presentation state and avoids optional unwrapping in the sheet body.5354### Sheets Own Their Actions5556**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.5758```swift59struct EditItemSheet: View {60@Environment(\.dismiss) private var dismiss61let item: Item62@State private var name: String6364init(item: Item) {65self.item = item66_name = State(initialValue: item.name)67}6869var body: some View {70NavigationStack {71Form { TextField("Name", text: $name) }72.navigationTitle("Edit Item")73.toolbar {74ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }75ToolbarItem(placement: .confirmationAction) { Button("Save") { /* save and dismiss */ } }76}77}78}79}80```8182### Enum-Based Sheet Management8384When presenting multiple different sheets, use an `Identifiable` enum with `.sheet(item:)` instead of multiple boolean state properties:8586```swift87struct ArticlesView: View {88enum Sheet: Identifiable {89case add, edit(Article), categories90var id: String {91switch self {92case .add: "add"93case .edit(let a): "edit-\(a.id)"94case .categories: "categories"95}96}97}9899@State private var presentedSheet: Sheet?100101var body: some View {102List { /* ... */ }103.toolbar {104Button("Add") { presentedSheet = .add }105}106.sheet(item: $presentedSheet) { sheet in107switch sheet {108case .add: AddArticleView()109case .edit(let article): EditArticleView(article: article)110case .categories: CategoriesView()111}112}113}114}115```116117**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.118119## Navigation Patterns120121### Type-Safe Navigation with NavigationStack122123```swift124struct ContentView: View {125var body: some View {126NavigationStack {127List {128NavigationLink("Profile", value: Route.profile)129NavigationLink("Settings", value: Route.settings)130}131.navigationDestination(for: Route.self) { route in132switch route {133case .profile:134ProfileView()135case .settings:136SettingsView()137}138}139}140}141}142143enum Route: Hashable {144case profile145case settings146}147```148149### Programmatic Navigation150151```swift152struct ContentView: View {153@State private var navigationPath = NavigationPath()154155var body: some View {156NavigationStack(path: $navigationPath) {157List {158Button("Go to Detail") {159navigationPath.append(DetailRoute.item(id: 1))160}161}162.navigationDestination(for: DetailRoute.self) { route in163switch route {164case .item(let id):165ItemDetailView(id: id)166}167}168}169}170}171172enum DetailRoute: Hashable {173case item(id: Int)174}175```176177## Multi-Column Navigation with NavigationSplitView178179### Two-Column Layout180181Use `NavigationSplitView` for sidebar-driven navigation. Available on iOS 16+, macOS 13+, tvOS 16+, watchOS 9+.182183```swift184struct ContentView: View {185@State private var selectedItem: Item.ID?186187var body: some View {188NavigationSplitView {189List(items, selection: $selectedItem) { item in190Text(item.name)191}192.navigationTitle("Items")193} detail: {194if let selectedItem, let item = items.first(where: { $0.id == selectedItem }) {195ItemDetailView(item: item)196} else {197ContentUnavailableView("Select an Item", systemImage: "doc")198}199}200}201}202```203204### Three-Column Layout205206```swift207struct ContentView: View {208@State private var departmentId: Department.ID?209@State private var employeeIds = Set<Employee.ID>()210211var body: some View {212NavigationSplitView {213List(model.departments, selection: $departmentId) { dept in214Text(dept.name)215}216} content: {217if let department = model.department(id: departmentId) {218List(department.employees, selection: $employeeIds) { emp in219Text(emp.name)220}221} else {222Text("Select a department")223}224} detail: {225EmployeeDetails(for: employeeIds)226}227}228}229```230231### Configuration232233- **Column visibility**: `NavigationSplitView(columnVisibility: $visibility)` with `NavigationSplitViewVisibility` (`.detailOnly`, `.doubleColumn`, `.all`)234- **Column widths**: `.navigationSplitViewColumnWidth(min:ideal:max:)` on each column235- **Compact column**: `NavigationSplitView(preferredCompactColumn: $column)` to control which column shows on narrow devices236- **Style**: `.navigationSplitViewStyle(.balanced)` or `.prominentDetail` (default)237238### Platform Behavior239240| Platform | Behavior |241|----------|----------|242| **macOS** | Columns always visible side-by-side; sidebar has translucent material; variable-width column resizing by dragging |243| **iPadOS (regular)** | Sidebar can overlay or push detail; supports column visibility toggle via toolbar button |244| **iOS / iPadOS (compact)** | Collapses into a single `NavigationStack`; sidebar items show disclosure chevrons; back button navigates between columns |245| **iPhone (all sizes)** | Always collapsed into a stack; sidebar appears as the root list; selections push detail onto the stack |246| **watchOS / tvOS** | Collapses into a single stack |247248## Inspector249250> **Availability:** iOS 17.0+, macOS 14.0+251252A trailing-edge panel for supplementary information.253254On wider size classes (macOS, iPad landscape), it appears as a **trailing column**. On compact size classes (iPhone), it **adapts to a sheet** automatically.255256### Basic Inspector257258```swift259struct ShapeEditor: View {260@State private var showInspector = false261262var body: some View {263MyEditorView()264.inspector(isPresented: $showInspector) {265InspectorContent()266}267.toolbar {268ToolbarItem {269Button {270showInspector.toggle()271} label: {272Label("Inspector", systemImage: "info.circle")273}274}275}276}277}278```279280### Inspector with Column Width281282```swift283MyEditorView()284.inspector(isPresented: $showInspector) {285InspectorContent()286.inspectorColumnWidth(min: 200, ideal: 250, max: 400)287}288```289290### Inspector with Fixed Width291292```swift293MyEditorView()294.inspector(isPresented: $showInspector) {295InspectorContent()296.inspectorColumnWidth(300)297}298```299300### Platform Behavior301302| Platform | Behavior |303|----------|----------|304| **macOS** | Trailing-edge sidebar panel; resizable by dragging edge; integrates with window toolbar |305| **iPadOS (regular)** | Trailing column alongside content; toggleable via toolbar button |306| **iOS / iPadOS (compact)** | Adapts to a sheet presentation; swipe-to-dismiss supported |307| **iPhone (all sizes)** | Always presented as a sheet (no trailing column); dismiss via swipe or button |308309> **Tip:** Use `InspectorCommands` in your app's `.commands` to include the default inspector toggle keyboard shortcut.310311## Presentation Modifiers312313### Full Screen Cover314315```swift316struct ContentView: View {317@State private var showFullScreen = false318319var body: some View {320Button("Show Full Screen") {321showFullScreen = true322}323.fullScreenCover(isPresented: $showFullScreen) {324FullScreenView()325}326}327}328```329330### Popover331332```swift333struct ContentView: View {334@State private var showPopover = false335336var body: some View {337Button("Show Popover") {338showPopover = true339}340.popover(isPresented: $showPopover) {341PopoverContentView()342.presentationCompactAdaptation(.popover) // Don't adapt to sheet on iPhone343}344}345}346```347348For `alert` and `confirmationDialog` API patterns, see `latest-apis.md`.349350## Summary Checklist351352- [ ] Use `.sheet(item:)` for model-based sheets353- [ ] Sheets own their actions and dismiss internally354- [ ] Use `NavigationStack` with `navigationDestination(for:)` for type-safe navigation355- [ ] Use `NavigationPath` for programmatic navigation356- [ ] Use `NavigationSplitView` for sidebar-driven multi-column layouts357- [ ] Use `Inspector` for trailing-edge supplementary panels358- [ ] Set column widths with `navigationSplitViewColumnWidth(min:ideal:max:)` or `inspectorColumnWidth(min:ideal:max:)`359- [ ] Use appropriate presentation modifiers (sheet, fullScreenCover, popover)360- [ ] Alerts and confirmation dialogs use modern API with actions361- [ ] Avoid passing dismiss/save callbacks to sheets362- [ ] Use enum-based `Identifiable` type with `.sheet(item:)` when presenting multiple sheets363- [ ] Navigation state can be saved/restored when needed364