swift-code-reviewer-skill 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -169
- package/README.md +43 -2
- package/SKILL.md +194 -711
- package/bin/install.js +1 -1
- package/package.json +2 -1
- package/references/agent-loop-feedback.md +148 -0
- package/references/companion-skills.md +70 -0
- package/references/spec-adherence.md +157 -0
- package/skills/README.md +43 -0
- package/skills/swift-concurrency/NOTICE.md +18 -0
- package/skills/swift-concurrency/SKILL.md +235 -0
- package/skills/swift-concurrency/references/actors.md +640 -0
- package/skills/swift-concurrency/references/async-await-basics.md +249 -0
- package/skills/swift-concurrency/references/async-sequences.md +635 -0
- package/skills/swift-concurrency/references/core-data.md +533 -0
- package/skills/swift-concurrency/references/glossary.md +96 -0
- package/skills/swift-concurrency/references/linting.md +38 -0
- package/skills/swift-concurrency/references/memory-management.md +542 -0
- package/skills/swift-concurrency/references/migration.md +721 -0
- package/skills/swift-concurrency/references/performance.md +574 -0
- package/skills/swift-concurrency/references/sendable.md +578 -0
- package/skills/swift-concurrency/references/tasks.md +604 -0
- package/skills/swift-concurrency/references/testing.md +565 -0
- package/skills/swift-concurrency/references/threading.md +452 -0
- package/skills/swift-expert/NOTICE.md +18 -0
- package/skills/swift-expert/SKILL.md +226 -0
- package/skills/swift-expert/references/async-concurrency.md +363 -0
- package/skills/swift-expert/references/memory-performance.md +380 -0
- package/skills/swift-expert/references/protocol-oriented.md +357 -0
- package/skills/swift-expert/references/swiftui-patterns.md +294 -0
- package/skills/swift-expert/references/testing-patterns.md +402 -0
- package/skills/swift-testing/NOTICE.md +18 -0
- package/skills/swift-testing/SKILL.md +295 -0
- package/skills/swift-testing/references/async-testing.md +245 -0
- package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
- package/skills/swift-testing/references/fixtures.md +193 -0
- package/skills/swift-testing/references/integration-testing.md +189 -0
- package/skills/swift-testing/references/migration-xctest.md +301 -0
- package/skills/swift-testing/references/parameterized-tests.md +171 -0
- package/skills/swift-testing/references/snapshot-testing.md +201 -0
- package/skills/swift-testing/references/test-doubles.md +243 -0
- package/skills/swift-testing/references/test-organization.md +231 -0
- package/skills/swiftui-expert-skill/NOTICE.md +18 -0
- package/skills/swiftui-expert-skill/SKILL.md +281 -0
- package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
- package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
- package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
- package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
- package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/skills/swiftui-expert-skill/references/state-management.md +417 -0
- package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
- package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
- package/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# SwiftUI Sheet, Navigation & Inspector Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [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)
|
|
11
|
+
|
|
12
|
+
## Sheet Patterns
|
|
13
|
+
|
|
14
|
+
### Item-Driven Sheets (Preferred)
|
|
15
|
+
|
|
16
|
+
**Use `.sheet(item:)` instead of `.sheet(isPresented:)` when presenting model-based content.**
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
// Good - item-driven
|
|
20
|
+
@State private var selectedItem: Item?
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
List(items) { item in
|
|
24
|
+
Button(item.name) {
|
|
25
|
+
selectedItem = item
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
.sheet(item: $selectedItem) { item in
|
|
29
|
+
ItemDetailSheet(item: item)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Avoid - boolean flag requires separate state
|
|
34
|
+
@State private var showSheet = false
|
|
35
|
+
@State private var selectedItem: Item?
|
|
36
|
+
|
|
37
|
+
var body: some View {
|
|
38
|
+
List(items) { item in
|
|
39
|
+
Button(item.name) {
|
|
40
|
+
selectedItem = item
|
|
41
|
+
showSheet = true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.sheet(isPresented: $showSheet) {
|
|
45
|
+
if let selectedItem {
|
|
46
|
+
ItemDetailSheet(item: selectedItem)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Why**: `.sheet(item:)` automatically handles presentation state and avoids optional unwrapping in the sheet body.
|
|
53
|
+
|
|
54
|
+
### Sheets Own Their Actions
|
|
55
|
+
|
|
56
|
+
**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.
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
struct EditItemSheet: View {
|
|
60
|
+
@Environment(\.dismiss) private var dismiss
|
|
61
|
+
let item: Item
|
|
62
|
+
@State private var name: String
|
|
63
|
+
|
|
64
|
+
init(item: Item) {
|
|
65
|
+
self.item = item
|
|
66
|
+
_name = State(initialValue: item.name)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var body: some View {
|
|
70
|
+
NavigationStack {
|
|
71
|
+
Form { TextField("Name", text: $name) }
|
|
72
|
+
.navigationTitle("Edit Item")
|
|
73
|
+
.toolbar {
|
|
74
|
+
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
|
|
75
|
+
ToolbarItem(placement: .confirmationAction) { Button("Save") { /* save and dismiss */ } }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Enum-Based Sheet Management
|
|
83
|
+
|
|
84
|
+
When presenting multiple different sheets, use an `Identifiable` enum with `.sheet(item:)` instead of multiple boolean state properties:
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
struct ArticlesView: View {
|
|
88
|
+
enum Sheet: Identifiable {
|
|
89
|
+
case add, edit(Article), categories
|
|
90
|
+
var id: String {
|
|
91
|
+
switch self {
|
|
92
|
+
case .add: "add"
|
|
93
|
+
case .edit(let a): "edit-\(a.id)"
|
|
94
|
+
case .categories: "categories"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@State private var presentedSheet: Sheet?
|
|
100
|
+
|
|
101
|
+
var body: some View {
|
|
102
|
+
List { /* ... */ }
|
|
103
|
+
.toolbar {
|
|
104
|
+
Button("Add") { presentedSheet = .add }
|
|
105
|
+
}
|
|
106
|
+
.sheet(item: $presentedSheet) { sheet in
|
|
107
|
+
switch sheet {
|
|
108
|
+
case .add: AddArticleView()
|
|
109
|
+
case .edit(let article): EditArticleView(article: article)
|
|
110
|
+
case .categories: CategoriesView()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**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.
|
|
118
|
+
|
|
119
|
+
## Navigation Patterns
|
|
120
|
+
|
|
121
|
+
### Type-Safe Navigation with NavigationStack
|
|
122
|
+
|
|
123
|
+
```swift
|
|
124
|
+
struct ContentView: View {
|
|
125
|
+
var body: some View {
|
|
126
|
+
NavigationStack {
|
|
127
|
+
List {
|
|
128
|
+
NavigationLink("Profile", value: Route.profile)
|
|
129
|
+
NavigationLink("Settings", value: Route.settings)
|
|
130
|
+
}
|
|
131
|
+
.navigationDestination(for: Route.self) { route in
|
|
132
|
+
switch route {
|
|
133
|
+
case .profile:
|
|
134
|
+
ProfileView()
|
|
135
|
+
case .settings:
|
|
136
|
+
SettingsView()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
enum Route: Hashable {
|
|
144
|
+
case profile
|
|
145
|
+
case settings
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Programmatic Navigation
|
|
150
|
+
|
|
151
|
+
```swift
|
|
152
|
+
struct ContentView: View {
|
|
153
|
+
@State private var navigationPath = NavigationPath()
|
|
154
|
+
|
|
155
|
+
var body: some View {
|
|
156
|
+
NavigationStack(path: $navigationPath) {
|
|
157
|
+
List {
|
|
158
|
+
Button("Go to Detail") {
|
|
159
|
+
navigationPath.append(DetailRoute.item(id: 1))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
.navigationDestination(for: DetailRoute.self) { route in
|
|
163
|
+
switch route {
|
|
164
|
+
case .item(let id):
|
|
165
|
+
ItemDetailView(id: id)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
enum DetailRoute: Hashable {
|
|
173
|
+
case item(id: Int)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Multi-Column Navigation with NavigationSplitView
|
|
178
|
+
|
|
179
|
+
### Two-Column Layout
|
|
180
|
+
|
|
181
|
+
Use `NavigationSplitView` for sidebar-driven navigation. Available on iOS 16+, macOS 13+, tvOS 16+, watchOS 9+.
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
struct ContentView: View {
|
|
185
|
+
@State private var selectedItem: Item.ID?
|
|
186
|
+
|
|
187
|
+
var body: some View {
|
|
188
|
+
NavigationSplitView {
|
|
189
|
+
List(items, selection: $selectedItem) { item in
|
|
190
|
+
Text(item.name)
|
|
191
|
+
}
|
|
192
|
+
.navigationTitle("Items")
|
|
193
|
+
} detail: {
|
|
194
|
+
if let selectedItem, let item = items.first(where: { $0.id == selectedItem }) {
|
|
195
|
+
ItemDetailView(item: item)
|
|
196
|
+
} else {
|
|
197
|
+
ContentUnavailableView("Select an Item", systemImage: "doc")
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Three-Column Layout
|
|
205
|
+
|
|
206
|
+
```swift
|
|
207
|
+
struct ContentView: View {
|
|
208
|
+
@State private var departmentId: Department.ID?
|
|
209
|
+
@State private var employeeIds = Set<Employee.ID>()
|
|
210
|
+
|
|
211
|
+
var body: some View {
|
|
212
|
+
NavigationSplitView {
|
|
213
|
+
List(model.departments, selection: $departmentId) { dept in
|
|
214
|
+
Text(dept.name)
|
|
215
|
+
}
|
|
216
|
+
} content: {
|
|
217
|
+
if let department = model.department(id: departmentId) {
|
|
218
|
+
List(department.employees, selection: $employeeIds) { emp in
|
|
219
|
+
Text(emp.name)
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
Text("Select a department")
|
|
223
|
+
}
|
|
224
|
+
} detail: {
|
|
225
|
+
EmployeeDetails(for: employeeIds)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Configuration
|
|
232
|
+
|
|
233
|
+
- **Column visibility**: `NavigationSplitView(columnVisibility: $visibility)` with `NavigationSplitViewVisibility` (`.detailOnly`, `.doubleColumn`, `.all`)
|
|
234
|
+
- **Column widths**: `.navigationSplitViewColumnWidth(min:ideal:max:)` on each column
|
|
235
|
+
- **Compact column**: `NavigationSplitView(preferredCompactColumn: $column)` to control which column shows on narrow devices
|
|
236
|
+
- **Style**: `.navigationSplitViewStyle(.balanced)` or `.prominentDetail` (default)
|
|
237
|
+
|
|
238
|
+
### Platform Behavior
|
|
239
|
+
|
|
240
|
+
| 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 |
|
|
247
|
+
|
|
248
|
+
## Inspector
|
|
249
|
+
|
|
250
|
+
> **Availability:** iOS 17.0+, macOS 14.0+
|
|
251
|
+
|
|
252
|
+
A trailing-edge panel for supplementary information.
|
|
253
|
+
|
|
254
|
+
On wider size classes (macOS, iPad landscape), it appears as a **trailing column**. On compact size classes (iPhone), it **adapts to a sheet** automatically.
|
|
255
|
+
|
|
256
|
+
### Basic Inspector
|
|
257
|
+
|
|
258
|
+
```swift
|
|
259
|
+
struct ShapeEditor: View {
|
|
260
|
+
@State private var showInspector = false
|
|
261
|
+
|
|
262
|
+
var body: some View {
|
|
263
|
+
MyEditorView()
|
|
264
|
+
.inspector(isPresented: $showInspector) {
|
|
265
|
+
InspectorContent()
|
|
266
|
+
}
|
|
267
|
+
.toolbar {
|
|
268
|
+
ToolbarItem {
|
|
269
|
+
Button {
|
|
270
|
+
showInspector.toggle()
|
|
271
|
+
} label: {
|
|
272
|
+
Label("Inspector", systemImage: "info.circle")
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Inspector with Column Width
|
|
281
|
+
|
|
282
|
+
```swift
|
|
283
|
+
MyEditorView()
|
|
284
|
+
.inspector(isPresented: $showInspector) {
|
|
285
|
+
InspectorContent()
|
|
286
|
+
.inspectorColumnWidth(min: 200, ideal: 250, max: 400)
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Inspector with Fixed Width
|
|
291
|
+
|
|
292
|
+
```swift
|
|
293
|
+
MyEditorView()
|
|
294
|
+
.inspector(isPresented: $showInspector) {
|
|
295
|
+
InspectorContent()
|
|
296
|
+
.inspectorColumnWidth(300)
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Platform Behavior
|
|
301
|
+
|
|
302
|
+
| 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 |
|
|
308
|
+
|
|
309
|
+
> **Tip:** Use `InspectorCommands` in your app's `.commands` to include the default inspector toggle keyboard shortcut.
|
|
310
|
+
|
|
311
|
+
## Presentation Modifiers
|
|
312
|
+
|
|
313
|
+
### Full Screen Cover
|
|
314
|
+
|
|
315
|
+
```swift
|
|
316
|
+
struct ContentView: View {
|
|
317
|
+
@State private var showFullScreen = false
|
|
318
|
+
|
|
319
|
+
var body: some View {
|
|
320
|
+
Button("Show Full Screen") {
|
|
321
|
+
showFullScreen = true
|
|
322
|
+
}
|
|
323
|
+
.fullScreenCover(isPresented: $showFullScreen) {
|
|
324
|
+
FullScreenView()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Popover
|
|
331
|
+
|
|
332
|
+
```swift
|
|
333
|
+
struct ContentView: View {
|
|
334
|
+
@State private var showPopover = false
|
|
335
|
+
|
|
336
|
+
var body: some View {
|
|
337
|
+
Button("Show Popover") {
|
|
338
|
+
showPopover = true
|
|
339
|
+
}
|
|
340
|
+
.popover(isPresented: $showPopover) {
|
|
341
|
+
PopoverContentView()
|
|
342
|
+
.presentationCompactAdaptation(.popover) // Don't adapt to sheet on iPhone
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
For `alert` and `confirmationDialog` API patterns, see `latest-apis.md`.
|
|
349
|
+
|
|
350
|
+
## Summary Checklist
|
|
351
|
+
|
|
352
|
+
- [ ] Use `.sheet(item:)` for model-based sheets
|
|
353
|
+
- [ ] Sheets own their actions and dismiss internally
|
|
354
|
+
- [ ] Use `NavigationStack` with `navigationDestination(for:)` for type-safe navigation
|
|
355
|
+
- [ ] Use `NavigationPath` for programmatic navigation
|
|
356
|
+
- [ ] Use `NavigationSplitView` for sidebar-driven multi-column layouts
|
|
357
|
+
- [ ] Use `Inspector` for trailing-edge supplementary panels
|
|
358
|
+
- [ ] 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 actions
|
|
361
|
+
- [ ] Avoid passing dismiss/save callbacks to sheets
|
|
362
|
+
- [ ] Use enum-based `Identifiable` type with `.sheet(item:)` when presenting multiple sheets
|
|
363
|
+
- [ ] Navigation state can be saved/restored when needed
|