swift-code-reviewer-skill 1.1.1 → 1.2.1

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +44 -162
  2. package/README.md +91 -21
  3. package/SKILL.md +107 -725
  4. package/bin/install.js +87 -22
  5. package/package.json +16 -2
  6. package/references/companion-skills.md +70 -0
  7. package/skills/README.md +43 -0
  8. package/skills/swift-concurrency/NOTICE.md +18 -0
  9. package/skills/swift-concurrency/SKILL.md +235 -0
  10. package/skills/swift-concurrency/references/actors.md +640 -0
  11. package/skills/swift-concurrency/references/async-await-basics.md +249 -0
  12. package/skills/swift-concurrency/references/async-sequences.md +635 -0
  13. package/skills/swift-concurrency/references/core-data.md +533 -0
  14. package/skills/swift-concurrency/references/glossary.md +96 -0
  15. package/skills/swift-concurrency/references/linting.md +38 -0
  16. package/skills/swift-concurrency/references/memory-management.md +542 -0
  17. package/skills/swift-concurrency/references/migration.md +721 -0
  18. package/skills/swift-concurrency/references/performance.md +574 -0
  19. package/skills/swift-concurrency/references/sendable.md +578 -0
  20. package/skills/swift-concurrency/references/tasks.md +604 -0
  21. package/skills/swift-concurrency/references/testing.md +565 -0
  22. package/skills/swift-concurrency/references/threading.md +452 -0
  23. package/skills/swift-expert/NOTICE.md +18 -0
  24. package/skills/swift-expert/SKILL.md +226 -0
  25. package/skills/swift-expert/references/async-concurrency.md +363 -0
  26. package/skills/swift-expert/references/memory-performance.md +380 -0
  27. package/skills/swift-expert/references/protocol-oriented.md +357 -0
  28. package/skills/swift-expert/references/swiftui-patterns.md +294 -0
  29. package/skills/swift-expert/references/testing-patterns.md +402 -0
  30. package/skills/swift-testing/NOTICE.md +18 -0
  31. package/skills/swift-testing/SKILL.md +295 -0
  32. package/skills/swift-testing/references/async-testing.md +245 -0
  33. package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
  34. package/skills/swift-testing/references/fixtures.md +193 -0
  35. package/skills/swift-testing/references/integration-testing.md +189 -0
  36. package/skills/swift-testing/references/migration-xctest.md +301 -0
  37. package/skills/swift-testing/references/parameterized-tests.md +171 -0
  38. package/skills/swift-testing/references/snapshot-testing.md +201 -0
  39. package/skills/swift-testing/references/test-doubles.md +243 -0
  40. package/skills/swift-testing/references/test-organization.md +231 -0
  41. package/skills/swiftui-expert-skill/NOTICE.md +18 -0
  42. package/skills/swiftui-expert-skill/SKILL.md +281 -0
  43. package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
  44. package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  45. package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  46. package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  47. package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  48. package/skills/swiftui-expert-skill/references/charts.md +602 -0
  49. package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  50. package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
  51. package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  52. package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
  53. package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
  54. package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  55. package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  56. package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  57. package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  58. package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  59. package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  60. package/skills/swiftui-expert-skill/references/state-management.md +417 -0
  61. package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
  62. package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
  63. package/skills/swiftui-ui-patterns/SKILL.md +95 -0
  64. package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  65. package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  66. package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  67. package/skills/swiftui-ui-patterns/references/controls.md +57 -0
  68. package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  69. package/skills/swiftui-ui-patterns/references/focus.md +90 -0
  70. package/skills/swiftui-ui-patterns/references/form.md +97 -0
  71. package/skills/swiftui-ui-patterns/references/grids.md +71 -0
  72. package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  73. package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  74. package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  75. package/skills/swiftui-ui-patterns/references/list.md +86 -0
  76. package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  77. package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  78. package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  79. package/skills/swiftui-ui-patterns/references/media.md +73 -0
  80. package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  81. package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  82. package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  83. package/skills/swiftui-ui-patterns/references/performance.md +62 -0
  84. package/skills/swiftui-ui-patterns/references/previews.md +48 -0
  85. package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  86. package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  87. package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  88. package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  89. package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  90. package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  91. package/skills/swiftui-ui-patterns/references/theming.md +71 -0
  92. package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  93. package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  94. package/templates/agents/swift-code-reviewer.md +78 -0
  95. package/templates/commands/review.md +56 -0
@@ -0,0 +1,394 @@
1
+ # SwiftUI List Patterns Reference
2
+
3
+ ## Table of Contents
4
+
5
+ - [ForEach Identity and Stability](#foreach-identity-and-stability)
6
+ - [Enumerated Sequences](#enumerated-sequences)
7
+ - [List with Custom Styling](#list-with-custom-styling)
8
+ - [List with Pull-to-Refresh](#list-with-pull-to-refresh)
9
+ - [Empty States with ContentUnavailableView (iOS 17+)](#empty-states-with-contentunavailableview-ios-17)
10
+ - [Custom List Backgrounds](#custom-list-backgrounds)
11
+ - [Table](#table)
12
+ - [Summary Checklist](#summary-checklist)
13
+
14
+ ## ForEach Identity and Stability
15
+
16
+ **Always provide stable identity for `ForEach`.** Never use `.indices` for dynamic content.
17
+
18
+ ```swift
19
+ // Good - stable identity via Identifiable
20
+ extension User: Identifiable {
21
+ var id: String { userId }
22
+ }
23
+
24
+ ForEach(users) { user in
25
+ UserRow(user: user)
26
+ }
27
+
28
+ // Good - stable identity via keypath
29
+ ForEach(users, id: \.userId) { user in
30
+ UserRow(user: user)
31
+ }
32
+
33
+ // Wrong - indices create static content
34
+ ForEach(users.indices, id: \.self) { index in
35
+ UserRow(user: users[index]) // Can crash on removal!
36
+ }
37
+
38
+ // Wrong - unstable identity
39
+ ForEach(users, id: \.self) { user in
40
+ UserRow(user: user) // Only works if User is Hashable and stable
41
+ }
42
+ ```
43
+
44
+ **Critical**: Ensure **constant number of views per element** in `ForEach`:
45
+
46
+ ```swift
47
+ // Good - consistent view count
48
+ ForEach(items) { item in
49
+ ItemRow(item: item)
50
+ }
51
+
52
+ // Bad - variable view count breaks identity
53
+ ForEach(items) { item in
54
+ if item.isSpecial {
55
+ SpecialRow(item: item)
56
+ DetailRow(item: item)
57
+ } else {
58
+ RegularRow(item: item)
59
+ }
60
+ }
61
+ ```
62
+
63
+ **Avoid inline filtering:**
64
+
65
+ ```swift
66
+ // Bad - unstable identity, changes on every update
67
+ ForEach(items.filter { $0.isEnabled }) { item in
68
+ ItemRow(item: item)
69
+ }
70
+
71
+ // Good - prefilter and cache
72
+ @State private var enabledItems: [Item] = []
73
+
74
+ var body: some View {
75
+ ForEach(enabledItems) { item in
76
+ ItemRow(item: item)
77
+ }
78
+ .onChange(of: items) { _, newItems in
79
+ enabledItems = newItems.filter { $0.isEnabled }
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Avoid `AnyView` in list rows:**
85
+
86
+ ```swift
87
+ // Bad - hides identity, increases cost
88
+ ForEach(items) { item in
89
+ AnyView(item.isSpecial ? SpecialRow(item: item) : RegularRow(item: item))
90
+ }
91
+
92
+ // Good - Create a unified row view
93
+ ForEach(items) { item in
94
+ ItemRow(item: item)
95
+ }
96
+
97
+ struct ItemRow: View {
98
+ let item: Item
99
+
100
+ var body: some View {
101
+ if item.isSpecial {
102
+ SpecialRow(item: item)
103
+ } else {
104
+ RegularRow(item: item)
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ **Why**: Stable identity is critical for performance and animations. Unstable identity causes excessive diffing, broken animations, and potential crashes.
111
+
112
+ ### Identifiable ID Must Be Truly Unique
113
+
114
+ Non-unique IDs cause SwiftUI to treat different items as identical, leading to duplicate rendering or missing views:
115
+
116
+ ```swift
117
+ // Bug -- two articles with the same URL show identical content
118
+ struct Article: Identifiable {
119
+ let title: String
120
+ let url: URL
121
+ var id: String { url.absoluteString } // Not unique if URLs repeat!
122
+ }
123
+
124
+ // Fix -- use a genuinely unique identifier
125
+ struct Article: Identifiable {
126
+ let id: UUID
127
+ let title: String
128
+ let url: URL
129
+ }
130
+ ```
131
+
132
+ **Classes get a default `ObjectIdentifier`-based `id`** when conforming to `Identifiable` without providing one. This is only unique for the object's lifetime and can be recycled after deallocation.
133
+
134
+ ## Enumerated Sequences
135
+
136
+ **Always convert enumerated sequences to arrays. To be able to use them in a ForEach.**
137
+
138
+ ```swift
139
+ let items = ["A", "B", "C"]
140
+
141
+ // Correct
142
+ ForEach(Array(items.enumerated()), id: \.offset) { index, item in
143
+ Text("\(index): \(item)")
144
+ }
145
+
146
+ // Wrong - Doesn't compile, enumerated() isn't an array
147
+ ForEach(items.enumerated(), id: \.offset) { index, item in
148
+ Text("\(index): \(item)")
149
+ }
150
+ ```
151
+
152
+ ## List with Custom Styling
153
+
154
+ ```swift
155
+ // Remove default background and separators
156
+ List(items) { item in
157
+ ItemRow(item: item)
158
+ .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
159
+ .listRowSeparator(.hidden)
160
+ }
161
+ .listStyle(.plain)
162
+ .scrollContentBackground(.hidden)
163
+ .background(Color.customBackground)
164
+ .environment(\.defaultMinListRowHeight, 1) // Allows custom row heights
165
+ ```
166
+
167
+ ## List with Pull-to-Refresh
168
+
169
+ ```swift
170
+ List(items) { item in
171
+ ItemRow(item: item)
172
+ }
173
+ .refreshable {
174
+ await loadItems()
175
+ }
176
+ ```
177
+
178
+ ## Empty States with ContentUnavailableView (iOS 17+)
179
+
180
+ Use `ContentUnavailableView` for empty list/search states. The built-in `.search` variant is auto-localized:
181
+
182
+ ```swift
183
+ List {
184
+ ForEach(searchResults) { item in
185
+ ItemRow(item: item)
186
+ }
187
+ }
188
+ .overlay {
189
+ if searchResults.isEmpty, !searchText.isEmpty {
190
+ ContentUnavailableView.search(text: searchText)
191
+ }
192
+ }
193
+ ```
194
+
195
+ For non-search empty states, use a custom instance:
196
+
197
+ ```swift
198
+ ContentUnavailableView(
199
+ "No Articles",
200
+ systemImage: "doc.richtext.fill",
201
+ description: Text("Articles you save will appear here.")
202
+ )
203
+ ```
204
+
205
+ ## Custom List Backgrounds
206
+
207
+ Use `.scrollContentBackground(.hidden)` to replace the default list background:
208
+
209
+ ```swift
210
+ List(items) { item in
211
+ ItemRow(item: item)
212
+ }
213
+ .scrollContentBackground(.hidden)
214
+ .background(Color.customBackground)
215
+ ```
216
+
217
+ Without `.scrollContentBackground(.hidden)`, a custom `.background()` has no visible effect on `List`.
218
+
219
+ ## Table
220
+
221
+ > **Availability:** iOS 16.0+, iPadOS 16.0+, visionOS 1.0+
222
+
223
+ A multi-column data container that presents rows of `Identifiable` data with sortable, selectable columns. On compact size classes (iPhone, iPad Slide Over), columns after the first are automatically hidden.
224
+
225
+ ### Basic Table
226
+
227
+ ```swift
228
+ struct Person: Identifiable {
229
+ let givenName: String
230
+ let familyName: String
231
+ let emailAddress: String
232
+ let id = UUID()
233
+ var fullName: String { givenName + " " + familyName }
234
+ }
235
+
236
+ struct PeopleTable: View {
237
+ @State private var people: [Person] = [ /* ... */ ]
238
+
239
+ var body: some View {
240
+ Table(people) {
241
+ TableColumn("Given Name", value: \.givenName)
242
+ TableColumn("Family Name", value: \.familyName)
243
+ TableColumn("E-Mail Address", value: \.emailAddress)
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ ### Table with Selection
250
+
251
+ Bind to a single `ID` for single-selection, or a `Set<ID>` for multi-selection:
252
+
253
+ ```swift
254
+ struct SelectableTable: View {
255
+ @State private var people: [Person] = [ /* ... */ ]
256
+ @State private var selectedPeople = Set<Person.ID>()
257
+
258
+ var body: some View {
259
+ Table(people, selection: $selectedPeople) {
260
+ TableColumn("Given Name", value: \.givenName)
261
+ TableColumn("Family Name", value: \.familyName)
262
+ TableColumn("E-Mail Address", value: \.emailAddress)
263
+ }
264
+ Text("\(selectedPeople.count) people selected")
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### Sortable Table
270
+
271
+ Provide a binding to `[KeyPathComparator]` and re-sort the data in `.onChange(of:)`:
272
+
273
+ ```swift
274
+ struct SortableTable: View {
275
+ @State private var people: [Person] = [ /* ... */ ]
276
+ @State private var sortOrder = [KeyPathComparator(\Person.givenName)]
277
+
278
+ var body: some View {
279
+ Table(people, sortOrder: $sortOrder) {
280
+ TableColumn("Given Name", value: \.givenName)
281
+ TableColumn("Family Name", value: \.familyName)
282
+ TableColumn("E-Mail Address", value: \.emailAddress)
283
+ }
284
+ .onChange(of: sortOrder) { _, newOrder in
285
+ people.sort(using: newOrder)
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+ **Important:** The table does **not** sort data itself — you must re-sort the collection when `sortOrder` changes.
292
+
293
+ ### Adaptive Table for Compact Size Classes
294
+
295
+ On iPhone or iPad in Slide Over, only the first column is shown. Customize it to display combined information:
296
+
297
+ ```swift
298
+ struct AdaptiveTable: View {
299
+ @Environment(\.horizontalSizeClass) private var horizontalSizeClass
300
+ private var isCompact: Bool { horizontalSizeClass == .compact }
301
+
302
+ @State private var people: [Person] = [ /* ... */ ]
303
+ @State private var sortOrder = [KeyPathComparator(\Person.givenName)]
304
+
305
+ var body: some View {
306
+ Table(people, sortOrder: $sortOrder) {
307
+ TableColumn("Given Name", value: \.givenName) { person in
308
+ VStack(alignment: .leading) {
309
+ Text(isCompact ? person.fullName : person.givenName)
310
+ if isCompact {
311
+ Text(person.emailAddress)
312
+ .foregroundStyle(.secondary)
313
+ }
314
+ }
315
+ }
316
+ TableColumn("Family Name", value: \.familyName)
317
+ TableColumn("E-Mail Address", value: \.emailAddress)
318
+ }
319
+ .onChange(of: sortOrder) { _, newOrder in
320
+ people.sort(using: newOrder)
321
+ }
322
+ }
323
+ }
324
+ ```
325
+
326
+ ### Table with Static Rows
327
+
328
+ Use `init(of:columns:rows:)` when rows are known at compile time:
329
+
330
+ ```swift
331
+ struct Purchase: Identifiable {
332
+ let price: Decimal
333
+ let id = UUID()
334
+ }
335
+
336
+ struct TipTable: View {
337
+ let currencyStyle = Decimal.FormatStyle.Currency(code: "USD")
338
+
339
+ var body: some View {
340
+ Table(of: Purchase.self) {
341
+ TableColumn("Base price") { purchase in
342
+ Text(purchase.price, format: currencyStyle)
343
+ }
344
+ TableColumn("With 15% tip") { purchase in
345
+ Text(purchase.price * 1.15, format: currencyStyle)
346
+ }
347
+ TableColumn("With 20% tip") { purchase in
348
+ Text(purchase.price * 1.2, format: currencyStyle)
349
+ }
350
+ } rows: {
351
+ TableRow(Purchase(price: 20))
352
+ TableRow(Purchase(price: 50))
353
+ TableRow(Purchase(price: 75))
354
+ }
355
+ }
356
+ }
357
+ ```
358
+
359
+ ### Table Styles
360
+
361
+ ```swift
362
+ // Inset (no borders)
363
+ Table(people) { /* columns */ }
364
+ .tableStyle(.inset)
365
+
366
+ // Hide column headers
367
+ Table(people) { /* columns */ }
368
+ .tableColumnHeaders(.hidden)
369
+ ```
370
+
371
+ ### Platform Behavior
372
+
373
+ | Platform | Behavior |
374
+ |----------|----------|
375
+ | **iPadOS (regular)** | Full multi-column layout; headers and all columns visible |
376
+ | **iPadOS (compact)** | Only the first column shown; headers hidden |
377
+ | **iPhone (all sizes)** | Only the first column shown; headers hidden; list-like appearance |
378
+
379
+ > **Best Practice:** Prefer handling the compact size class by showing combined info in the first column. This provides a seamless transition when the size class changes (e.g., entering/exiting Slide Over on iPad).
380
+
381
+ ## Summary Checklist
382
+
383
+ - [ ] ForEach uses stable identity (never `.indices` for dynamic content)
384
+ - [ ] Identifiable IDs are truly unique across all items
385
+ - [ ] Constant number of views per ForEach element
386
+ - [ ] No inline filtering in ForEach (prefilter and cache instead)
387
+ - [ ] No `AnyView` in list rows
388
+ - [ ] Don't convert enumerated sequences to arrays
389
+ - [ ] Use `.refreshable` for pull-to-refresh
390
+ - [ ] Use `ContentUnavailableView` for empty states (iOS 17+)
391
+ - [ ] Use `.scrollContentBackground(.hidden)` for custom list backgrounds
392
+ - [ ] `Table` adapts for compact size classes (first column shows combined info)
393
+ - [ ] `Table` sorting re-sorts data in `.onChange(of: sortOrder)` (table doesn't sort itself)
394
+ - [ ] `Table` data conforms to `Identifiable`
@@ -0,0 +1,318 @@
1
+ # macOS Scenes Reference
2
+
3
+ > SwiftUI scene types for macOS apps — `Settings`, `MenuBarExtra`, `WindowGroup`, `Window`, `UtilityWindow`, and `DocumentGroup`. Covers macOS-only scenes and cross-platform scenes with macOS-specific behavior.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Quick Lookup Table](#quick-lookup-table)
8
+ - [Settings (macOS-only)](#settings-macos-only)
9
+ - [MenuBarExtra (macOS-only)](#menubarextra-macos-only)
10
+ - [WindowGroup (macOS behavior)](#windowgroup-macos-behavior)
11
+ - [Window](#window)
12
+ - [UtilityWindow (macOS-only)](#utilitywindow-macos-only)
13
+ - [DocumentGroup](#documentgroup)
14
+ - [Platform Conditionals](#platform-conditionals)
15
+ - [Best Practices](#best-practices)
16
+
17
+ ---
18
+
19
+ ## Quick Lookup Table
20
+
21
+ | API | Availability | macOS-Only? | macOS-Specific Behavior |
22
+ |-----|-------------|:-----------:|------------------------|
23
+ | `WindowGroup` | macOS 11.0+ | No | Multiple window instances, tabbed interface, automatic Window menu commands |
24
+ | `Window` | macOS 13.0+ | No | App quits when sole window closes; adds itself to Windows menu |
25
+ | `UtilityWindow` | macOS 15.0+ | Yes | Floating tool palette; receives `FocusedValues` from active main window |
26
+ | `Settings` | macOS 11.0+ | Yes | Presents preferences window (Cmd+,) |
27
+ | `MenuBarExtra` | macOS 13.0+ | Yes | Persistent icon/menu in the system menu bar |
28
+ | `DocumentGroup` | macOS 11.0+ | No | Document-based menu bar commands (File > New/Open/Save); multiple document windows |
29
+
30
+ ---
31
+
32
+ ## Settings (macOS-only)
33
+
34
+ Presents the app's preferences window, accessible via **Cmd+,** or the app menu. SwiftUI automatically enables the Settings menu item and manages the window lifecycle.
35
+
36
+ ```swift
37
+ Settings {
38
+ TabView {
39
+ Tab("General", systemImage: "gear") { GeneralSettingsView() }
40
+ Tab("Advanced", systemImage: "star") { AdvancedSettingsView() }
41
+ }
42
+ .scenePadding()
43
+ .frame(maxWidth: 350, minHeight: 100)
44
+ }
45
+ ```
46
+
47
+ Use `TabView` with `Tab` items for multi-pane preferences. Each tab's content is typically a `Form` with `@AppStorage`-backed controls.
48
+
49
+ ### SettingsLink (macOS 14.0+)
50
+
51
+ A button that opens the Settings scene. Use for in-app navigation to preferences.
52
+
53
+ ```swift
54
+ struct SidebarFooter: View {
55
+ var body: some View {
56
+ SettingsLink {
57
+ Label("Preferences", systemImage: "gear")
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### openSettings environment action (macOS 14.0+)
64
+
65
+ Programmatically open (or bring to front) the Settings window.
66
+
67
+ ```swift
68
+ struct OpenSettingsButton: View {
69
+ @Environment(\.openSettings) private var openSettings
70
+
71
+ var body: some View {
72
+ Button("Open Settings") {
73
+ openSettings()
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## MenuBarExtra (macOS-only)
82
+
83
+ Renders a persistent control in the system menu bar. Two styles available:
84
+ - **`.menu`** (default) — standard dropdown menu
85
+ - **`.window`** — popover panel with custom SwiftUI views
86
+
87
+ ### Menu-style (dropdown)
88
+
89
+ ```swift
90
+ MenuBarExtra("My Utility", systemImage: "hammer") {
91
+ Button("Action One") { /* ... */ }
92
+ Button("Action Two") { /* ... */ }
93
+ Divider()
94
+ Button("Quit") { NSApplication.shared.terminate(nil) }
95
+ }
96
+ ```
97
+
98
+ ### Window-style (popover panel)
99
+
100
+ ```swift
101
+ MenuBarExtra("Status", systemImage: "chart.bar") {
102
+ DashboardView()
103
+ .frame(width: 240)
104
+ }
105
+ .menuBarExtraStyle(.window)
106
+ ```
107
+
108
+ **Variations:**
109
+ - **Toggleable** — pass `isInserted:` with an `@AppStorage` binding to let users show/hide the extra: `MenuBarExtra("Status", systemImage: "chart.bar", isInserted: $showMenuBarExtra)`
110
+ - **Menu-bar-only app** — use `MenuBarExtra` as the sole scene + set `LSUIElement = true` in Info.plist to hide the Dock icon. The app auto-terminates if the user removes the extra from the menu bar.
111
+
112
+ ---
113
+
114
+ ## WindowGroup (macOS behavior)
115
+
116
+ On macOS, `WindowGroup` supports:
117
+ - **Multiple window instances** — users can open many windows from File > New Window
118
+ - **Tabbed interface** — users can merge windows into tabs
119
+ - **Automatic Window menu** — commands for window management appear automatically
120
+
121
+ ```swift
122
+ @main
123
+ struct Mail: App {
124
+ var body: some Scene {
125
+ // Basic multi-window support
126
+ WindowGroup {
127
+ MailViewer()
128
+ }
129
+
130
+ // Data-presenting window opened programmatically
131
+ WindowGroup("Message", for: Message.ID.self) { $messageID in
132
+ MessageDetail(messageID: messageID)
133
+ }
134
+ }
135
+ }
136
+
137
+ // Open a specific window programmatically
138
+ struct NewMessageButton: View {
139
+ var message: Message
140
+ @Environment(\.openWindow) private var openWindow
141
+
142
+ var body: some View {
143
+ Button("Open Message") {
144
+ openWindow(value: message.id)
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ > **Key difference from `Window`:** `WindowGroup` keeps the app running even after all windows are closed. `Window` (as sole scene) quits the app when closed.
151
+
152
+ ---
153
+
154
+ ## Window
155
+
156
+ A single, unique window scene. The system ensures only one instance exists.
157
+
158
+ ```swift
159
+ @main
160
+ struct Mail: App {
161
+ var body: some Scene {
162
+ WindowGroup {
163
+ MailViewer()
164
+ }
165
+
166
+ // Supplementary singleton window
167
+ Window("Connection Doctor", id: "connection-doctor") {
168
+ ConnectionDoctor()
169
+ }
170
+ }
171
+ }
172
+
173
+ // Open programmatically — brings to front if already open
174
+ struct OpenDoctorButton: View {
175
+ @Environment(\.openWindow) private var openWindow
176
+
177
+ var body: some View {
178
+ Button("Connection Doctor") {
179
+ openWindow(id: "connection-doctor")
180
+ }
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Window as sole scene
186
+
187
+ If `Window` is the only scene, the app quits when the window closes:
188
+
189
+ ```swift
190
+ @main
191
+ struct VideoCall: App {
192
+ var body: some Scene {
193
+ Window("VideoCall", id: "main") {
194
+ CameraView()
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ > **Recommendation:** In most cases, prefer `WindowGroup` for the primary scene. Use `Window` for supplementary singleton windows.
201
+
202
+ ---
203
+
204
+ ## UtilityWindow (macOS-only)
205
+
206
+ A specialized floating window for tool palettes and inspector panels. Available since macOS 15.0.
207
+
208
+ **Key behaviors:**
209
+ - Receives `FocusedValues` from the focused main scene (like menu bar commands)
210
+ - Floats above main windows (default level: `.floating`)
211
+ - Hides when the app is no longer active
212
+ - Only becomes focused when explicitly needed (e.g., clicking the title bar)
213
+ - Dismissible with the Escape key
214
+ - Not minimizable by default
215
+ - Automatically adds a show/hide item to the View menu
216
+
217
+ ```swift
218
+ @main
219
+ struct PhotoBrowser: App {
220
+ var body: some Scene {
221
+ WindowGroup {
222
+ PhotoGallery()
223
+ }
224
+
225
+ UtilityWindow("Photo Info", id: "photo-info") {
226
+ PhotoInfoViewer()
227
+ }
228
+ }
229
+ }
230
+
231
+ struct PhotoInfoViewer: View {
232
+ // Automatically updates based on whichever main window is focused
233
+ @FocusedValue(PhotoSelection.self) private var selectedPhotos
234
+
235
+ var body: some View {
236
+ if let photos = selectedPhotos {
237
+ Text("\(photos.count) photos selected")
238
+ } else {
239
+ Text("No selection")
240
+ .foregroundStyle(.secondary)
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ > **Tip:** Remove the automatic View menu item with `.commandsRemoved()` and place a `WindowVisibilityToggle` elsewhere in your commands.
247
+
248
+ ---
249
+
250
+ ## DocumentGroup
251
+
252
+ Document-based apps with automatic file management. On macOS, provides:
253
+ - **Document-based menu bar commands** (File > New, Open, Save, Revert)
254
+ - **Multiple document windows** simultaneously
255
+ - On iOS, shows a document browser instead
256
+
257
+ ```swift
258
+ DocumentGroup(newDocument: TextFile()) { config in
259
+ ContentView(document: config.$document)
260
+ }
261
+ ```
262
+
263
+ The document type must conform to `FileDocument` (value type) or `ReferenceFileDocument` (reference type). Key requirements:
264
+
265
+ ```swift
266
+ struct TextFile: FileDocument {
267
+ static var readableContentTypes: [UTType] { [.plainText] }
268
+ var text: String = ""
269
+ init() {}
270
+ init(configuration: ReadConfiguration) throws {
271
+ text = String(data: configuration.file.regularFileContents ?? Data(), encoding: .utf8) ?? ""
272
+ }
273
+ func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
274
+ FileWrapper(regularFileWithContents: Data(text.utf8))
275
+ }
276
+ }
277
+ ```
278
+
279
+ For multiple document types, add additional `DocumentGroup` scenes — use `DocumentGroup(viewing:)` for read-only formats.
280
+
281
+ ---
282
+
283
+ ## Platform Conditionals
284
+
285
+ Always wrap macOS-only scenes in `#if os(macOS)`:
286
+
287
+ ```swift
288
+ @main
289
+ struct MyApp: App {
290
+ var body: some Scene {
291
+ WindowGroup {
292
+ ContentView()
293
+ }
294
+
295
+ #if os(macOS)
296
+ Settings {
297
+ SettingsView()
298
+ }
299
+
300
+ MenuBarExtra("Status", systemImage: "bolt") {
301
+ StatusMenu()
302
+ }
303
+ #endif
304
+ }
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ## Best Practices
311
+
312
+ - **Use `Settings`** for preferences — prefer this over a custom preferences window
313
+ - **Use `MenuBarExtra`** for menu bar items — prefer this over managing AppKit's `NSStatusItem` directly
314
+ - **Use `WindowGroup`** as the primary scene — reserve `Window` for supplementary singletons
315
+ - **Use `UtilityWindow`** for inspectors/palettes — it handles floating, focus, and visibility automatically
316
+ - **Use `DocumentGroup`** for document-based apps — it provides the full File menu and document lifecycle
317
+ - **Gate macOS-only scenes** with `#if os(macOS)` for multiplatform projects
318
+ - **Use `openWindow(id:)`** to open windows programmatically — it brings existing windows to front