swift-code-reviewer-skill 1.2.0 → 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.
- package/CHANGELOG.md +35 -169
- package/README.md +43 -2
- package/SKILL.md +107 -742
- package/bin/install.js +1 -1
- package/package.json +2 -1
- package/references/companion-skills.md +70 -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,357 @@
|
|
|
1
|
+
# macOS Views & Components Reference
|
|
2
|
+
|
|
3
|
+
> macOS-specific SwiftUI views, file operations, drag & drop, and AppKit interop. Covers `HSplitView`, `VSplitView`, `Table`, `PasteButton`, file dialogs, cross-app drag & drop, and `NSViewRepresentable`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Lookup Table](#quick-lookup-table)
|
|
8
|
+
- [HSplitView & VSplitView (macOS-only)](#hsplitview--vsplitview-macos-only)
|
|
9
|
+
- [Table](#table)
|
|
10
|
+
- [PasteButton & CopyButton](#pastebutton--copybutton)
|
|
11
|
+
- [File Operations](#file-operations)
|
|
12
|
+
- [Drag, Drop & Pasteboard](#drag-drop--pasteboard)
|
|
13
|
+
- [AppKit Interop](#appkit-interop)
|
|
14
|
+
- [Best Practices](#best-practices)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quick Lookup Table
|
|
19
|
+
|
|
20
|
+
### Views
|
|
21
|
+
|
|
22
|
+
| API | Availability | macOS-Only? | Usage |
|
|
23
|
+
|-----|-------------|:-----------:|-------|
|
|
24
|
+
| `HSplitView` | macOS 10.15+ | Yes | Horizontal resizable split layout with user-draggable dividers |
|
|
25
|
+
| `VSplitView` | macOS 10.15+ | Yes | Vertical resizable split layout with user-draggable dividers |
|
|
26
|
+
| `Table` | macOS 12.0+ | No | Full multi-column layout with sorting; on iOS compact, columns collapse |
|
|
27
|
+
| `PasteButton` | macOS 10.15+ | No | System button that reads clipboard; does NOT auto-validate on macOS |
|
|
28
|
+
| `CopyButton` | macOS 15.0+ | Yes | System button that copies `Transferable` content to clipboard |
|
|
29
|
+
|
|
30
|
+
### File Operations
|
|
31
|
+
|
|
32
|
+
| API | Availability | macOS-Only? | Usage |
|
|
33
|
+
|-----|-------------|:-----------:|-------|
|
|
34
|
+
| `fileImporter()` | macOS 11.0+ | No | Native NSOpenPanel with column/list/gallery view, sidebar, tags, QuickLook |
|
|
35
|
+
| `fileExporter()` | macOS 11.0+ | No | Native NSSavePanel with format dropdown, tags field |
|
|
36
|
+
| `fileMover()` | macOS 11.0+ | No | Native macOS move panel with Finder-like navigation |
|
|
37
|
+
| `fileDialogMessage(_:)` | macOS 13.0+ | Yes | Custom message text in file dialogs |
|
|
38
|
+
| `fileDialogConfirmationLabel(_:)` | macOS 13.0+ | Yes | Custom confirm button text in file dialogs |
|
|
39
|
+
| `fileExporterFilenameLabel(_:)` | macOS 13.0+ | Yes | Custom filename field label in file exporter |
|
|
40
|
+
|
|
41
|
+
### Drag, Drop & Pasteboard
|
|
42
|
+
|
|
43
|
+
| API | Availability | macOS-Only? | Usage |
|
|
44
|
+
|-----|-------------|:-----------:|-------|
|
|
45
|
+
| `onDrag(_:)` / `draggable(_:)` | macOS 11.0+ | No | Drag image follows cursor; items draggable between apps |
|
|
46
|
+
| `onDrop(of:delegate:)` / `dropDestination(for:action:)` | macOS 11.0+ | No | Accepts drops from any macOS app including Finder |
|
|
47
|
+
|
|
48
|
+
### AppKit Interop
|
|
49
|
+
|
|
50
|
+
| API | Availability | macOS-Only? | Usage |
|
|
51
|
+
|-----|-------------|:-----------:|-------|
|
|
52
|
+
| `NSViewRepresentable` | macOS 10.15+ | Yes | Wrap an AppKit `NSView` in SwiftUI |
|
|
53
|
+
| `NSViewControllerRepresentable` | macOS 10.15+ | Yes | Wrap an AppKit `NSViewController` in SwiftUI |
|
|
54
|
+
| `NSHostingController` | macOS 10.15+ | Yes | Host SwiftUI inside an AppKit view controller |
|
|
55
|
+
| `NSHostingView` | macOS 10.15+ | Yes | Host SwiftUI inside an AppKit `NSView` hierarchy |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## HSplitView & VSplitView (macOS-only)
|
|
60
|
+
|
|
61
|
+
Resizable split layouts with user-draggable dividers. Use for IDE-style panes where all panels are equal peers. `VSplitView` works identically but splits vertically (use `minHeight` instead).
|
|
62
|
+
|
|
63
|
+
```swift
|
|
64
|
+
HSplitView {
|
|
65
|
+
FileTreeView()
|
|
66
|
+
.frame(minWidth: 200)
|
|
67
|
+
CodeEditorView()
|
|
68
|
+
.frame(minWidth: 400)
|
|
69
|
+
PreviewPane()
|
|
70
|
+
.frame(minWidth: 200)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> **When to use which:**
|
|
75
|
+
> - **`NavigationSplitView`** — sidebar-based navigation (sidebar drives content/detail)
|
|
76
|
+
> - **`HSplitView`/`VSplitView`** — IDE-style layouts where all panes are equal peers
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Table
|
|
81
|
+
|
|
82
|
+
For `Table` basics (creation, selection, sorting, adaptive compact layout), see `list-patterns.md`. This section covers macOS-specific table styling.
|
|
83
|
+
|
|
84
|
+
### Table styles
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
// Bordered with visible grid lines (macOS-only)
|
|
88
|
+
Table(people) { /* columns */ }
|
|
89
|
+
.tableStyle(.bordered)
|
|
90
|
+
|
|
91
|
+
// Bordered with alternating row backgrounds
|
|
92
|
+
Table(people) { /* columns */ }
|
|
93
|
+
.tableStyle(.bordered(alternatesRowBackgrounds: true))
|
|
94
|
+
|
|
95
|
+
// Inset (no borders)
|
|
96
|
+
Table(people) { /* columns */ }
|
|
97
|
+
.tableStyle(.inset)
|
|
98
|
+
|
|
99
|
+
// Hide column headers
|
|
100
|
+
Table(people) { /* columns */ }
|
|
101
|
+
.tableColumnHeaders(.hidden)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## PasteButton & CopyButton
|
|
107
|
+
|
|
108
|
+
### PasteButton
|
|
109
|
+
|
|
110
|
+
System button that reads clipboard content via `Transferable`. On macOS, it does NOT auto-validate pasteboard changes (unlike iOS).
|
|
111
|
+
|
|
112
|
+
```swift
|
|
113
|
+
struct ClipboardView: View {
|
|
114
|
+
@State private var pastedText = ""
|
|
115
|
+
|
|
116
|
+
var body: some View {
|
|
117
|
+
HStack {
|
|
118
|
+
PasteButton(payloadType: String.self) { strings in
|
|
119
|
+
pastedText = strings[0]
|
|
120
|
+
}
|
|
121
|
+
Divider()
|
|
122
|
+
Text(pastedText)
|
|
123
|
+
Spacer()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### CopyButton (macOS 15.0+, macOS-only)
|
|
130
|
+
|
|
131
|
+
System button that copies `Transferable` content to the clipboard.
|
|
132
|
+
|
|
133
|
+
```swift
|
|
134
|
+
struct CopyableContent: View {
|
|
135
|
+
let shareableText = "Hello, world!"
|
|
136
|
+
|
|
137
|
+
var body: some View {
|
|
138
|
+
HStack {
|
|
139
|
+
Text(shareableText)
|
|
140
|
+
CopyButton(item: shareableText)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## File Operations
|
|
149
|
+
|
|
150
|
+
### fileImporter
|
|
151
|
+
|
|
152
|
+
On macOS, presents a native `NSOpenPanel` with column/list/gallery view, sidebar favorites, tags, and QuickLook.
|
|
153
|
+
|
|
154
|
+
```swift
|
|
155
|
+
.fileImporter(
|
|
156
|
+
isPresented: $showImporter,
|
|
157
|
+
allowedContentTypes: [.pdf],
|
|
158
|
+
allowsMultipleSelection: false
|
|
159
|
+
) { result in
|
|
160
|
+
if case .success(let urls) = result, let url = urls.first {
|
|
161
|
+
guard url.startAccessingSecurityScopedResource() else { return }
|
|
162
|
+
defer { url.stopAccessingSecurityScopedResource() }
|
|
163
|
+
// use url
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> **Important:** Always call `startAccessingSecurityScopedResource()` on returned URLs, and `stopAccessingSecurityScopedResource()` when done. These are security-scoped bookmarks — access fails without this.
|
|
169
|
+
|
|
170
|
+
### fileExporter
|
|
171
|
+
|
|
172
|
+
On macOS, presents a native `NSSavePanel` with format dropdown and tags.
|
|
173
|
+
|
|
174
|
+
```swift
|
|
175
|
+
.fileExporter(
|
|
176
|
+
isPresented: $showExporter,
|
|
177
|
+
document: document,
|
|
178
|
+
contentType: .plainText,
|
|
179
|
+
defaultFilename: "MyFile.txt"
|
|
180
|
+
) { result in
|
|
181
|
+
// handle Result<URL, Error>
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### File dialog customization (macOS-only)
|
|
186
|
+
|
|
187
|
+
Customize text in file dialogs with these macOS-specific modifiers:
|
|
188
|
+
|
|
189
|
+
```swift
|
|
190
|
+
// Custom message and confirm button on file importer
|
|
191
|
+
.fileImporter(
|
|
192
|
+
isPresented: $showImporter,
|
|
193
|
+
allowedContentTypes: [.image]
|
|
194
|
+
) { result in
|
|
195
|
+
// handle result
|
|
196
|
+
}
|
|
197
|
+
.fileDialogMessage("Select an image to use as your profile photo")
|
|
198
|
+
.fileDialogConfirmationLabel("Use This Photo")
|
|
199
|
+
|
|
200
|
+
// Custom filename label on file exporter
|
|
201
|
+
.fileExporter(
|
|
202
|
+
isPresented: $showExporter,
|
|
203
|
+
document: myDocument,
|
|
204
|
+
contentType: .png
|
|
205
|
+
) { result in
|
|
206
|
+
// handle result
|
|
207
|
+
}
|
|
208
|
+
.fileExporterFilenameLabel("Export As:")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Drag, Drop & Pasteboard
|
|
214
|
+
|
|
215
|
+
On macOS, drag and drop works **across applications** (e.g., drag from your app to Finder, Mail, or other apps).
|
|
216
|
+
|
|
217
|
+
### Modern approach (Transferable)
|
|
218
|
+
|
|
219
|
+
```swift
|
|
220
|
+
// Drag source
|
|
221
|
+
struct DraggableCard: View {
|
|
222
|
+
let item: MyItem
|
|
223
|
+
|
|
224
|
+
var body: some View {
|
|
225
|
+
Text(item.title)
|
|
226
|
+
.draggable(item) // Requires Transferable conformance
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Drop target
|
|
231
|
+
struct DropZone: View {
|
|
232
|
+
@State private var droppedItems: [MyItem] = []
|
|
233
|
+
|
|
234
|
+
var body: some View {
|
|
235
|
+
VStack {
|
|
236
|
+
ForEach(droppedItems) { item in
|
|
237
|
+
Text(item.title)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
.dropDestination(for: MyItem.self) { items, location in
|
|
241
|
+
droppedItems.append(contentsOf: items)
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
244
|
+
.frame(width: 300, height: 200)
|
|
245
|
+
.border(.secondary)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Legacy approach (NSItemProvider)
|
|
251
|
+
|
|
252
|
+
```swift
|
|
253
|
+
// Drag source
|
|
254
|
+
Image(systemName: "doc")
|
|
255
|
+
.onDrag {
|
|
256
|
+
NSItemProvider(object: fileURL as NSURL)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Drop target
|
|
260
|
+
Text("Drop files here")
|
|
261
|
+
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
|
262
|
+
// handle providers
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## AppKit Interop
|
|
270
|
+
|
|
271
|
+
### NSViewRepresentable (macOS-only)
|
|
272
|
+
|
|
273
|
+
Wraps an AppKit `NSView` for use in SwiftUI. Implement `makeNSView(context:)` and `updateNSView(_:context:)`.
|
|
274
|
+
|
|
275
|
+
```swift
|
|
276
|
+
struct WebView: NSViewRepresentable {
|
|
277
|
+
let url: URL
|
|
278
|
+
func makeNSView(context: Context) -> WKWebView { WKWebView() }
|
|
279
|
+
func updateNSView(_ nsView: WKWebView, context: Context) {
|
|
280
|
+
nsView.load(URLRequest(url: url))
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### NSViewRepresentable with Coordinator
|
|
286
|
+
|
|
287
|
+
Use a Coordinator to forward delegate/target-action callbacks to SwiftUI.
|
|
288
|
+
|
|
289
|
+
```swift
|
|
290
|
+
struct SearchField: NSViewRepresentable {
|
|
291
|
+
@Binding var text: String
|
|
292
|
+
|
|
293
|
+
func makeNSView(context: Context) -> NSSearchField {
|
|
294
|
+
let field = NSSearchField()
|
|
295
|
+
field.delegate = context.coordinator
|
|
296
|
+
return field
|
|
297
|
+
}
|
|
298
|
+
func updateNSView(_ nsView: NSSearchField, context: Context) {
|
|
299
|
+
nsView.stringValue = text
|
|
300
|
+
}
|
|
301
|
+
func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
|
|
302
|
+
|
|
303
|
+
class Coordinator: NSObject, NSSearchFieldDelegate {
|
|
304
|
+
var text: Binding<String>
|
|
305
|
+
init(text: Binding<String>) { self.text = text }
|
|
306
|
+
func controlTextDidChange(_ obj: Notification) {
|
|
307
|
+
if let field = obj.object as? NSSearchField {
|
|
308
|
+
text.wrappedValue = field.stringValue
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
> **Warning:** Never set `frame`/`bounds` directly on the managed `NSView` — SwiftUI owns the layout.
|
|
316
|
+
|
|
317
|
+
### NSViewControllerRepresentable (macOS-only)
|
|
318
|
+
|
|
319
|
+
Wraps an AppKit `NSViewController` for use in SwiftUI.
|
|
320
|
+
|
|
321
|
+
```swift
|
|
322
|
+
struct MapViewWrapper: NSViewControllerRepresentable {
|
|
323
|
+
func makeNSViewController(context: Context) -> MapViewController {
|
|
324
|
+
MapViewController()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
|
|
328
|
+
// Update the controller when SwiftUI state changes
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### NSHostingController & NSHostingView (macOS-only)
|
|
334
|
+
|
|
335
|
+
Host SwiftUI content inside AppKit (reverse direction — AppKit app embedding SwiftUI views).
|
|
336
|
+
|
|
337
|
+
```swift
|
|
338
|
+
// Host SwiftUI as a view controller
|
|
339
|
+
let hostingController = NSHostingController(rootView: MySwiftUIView())
|
|
340
|
+
window.contentViewController = hostingController
|
|
341
|
+
|
|
342
|
+
// Host SwiftUI directly as an NSView
|
|
343
|
+
let hostingView = NSHostingView(rootView: MySwiftUIView())
|
|
344
|
+
someNSView.addSubview(hostingView)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Best Practices
|
|
350
|
+
|
|
351
|
+
- **Use `NavigationSplitView`** for sidebar-driven navigation — reserve `HSplitView`/`VSplitView` for IDE-style equal peer panes
|
|
352
|
+
- **Make `Table` adaptive** — handle compact size classes by showing combined info in the first column
|
|
353
|
+
- **Always call `startAccessingSecurityScopedResource()`** on URLs from `fileImporter` — they are security-scoped
|
|
354
|
+
- **Use `Transferable`** for drag & drop (modern) — fall back to `NSItemProvider` only for legacy compatibility
|
|
355
|
+
- **Use `NSViewRepresentable` with Coordinator** when you need delegate callbacks from AppKit views
|
|
356
|
+
- **Never set `frame`/`bounds`** directly on views managed by `NSViewRepresentable` — SwiftUI owns the layout
|
|
357
|
+
- **Prefer native SwiftUI** over AppKit interop when possible — only use `NSViewRepresentable` for features SwiftUI doesn't provide
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# macOS Window & Toolbar Styling Reference
|
|
2
|
+
|
|
3
|
+
> Window configuration, toolbar styles, sizing, positioning, and navigation patterns specific to macOS SwiftUI apps.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Lookup Table](#quick-lookup-table)
|
|
8
|
+
- [Toolbar Styles](#toolbar-styles)
|
|
9
|
+
- [Window Style](#window-style)
|
|
10
|
+
- [Window Sizing](#window-sizing)
|
|
11
|
+
- [MenuBarExtra Style (macOS-only)](#menubarextra-style-macos-only)
|
|
12
|
+
- [Navigation Layout (macOS behavior)](#navigation-layout-macos-behavior)
|
|
13
|
+
- [Commands & Keyboard](#commands--keyboard)
|
|
14
|
+
- [Best Practices](#best-practices)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quick Lookup Table
|
|
19
|
+
|
|
20
|
+
| API | Availability | macOS-Only? | Usage |
|
|
21
|
+
|-----|-------------|:-----------:|-------|
|
|
22
|
+
| `windowToolbarStyle(_:)` | macOS 11.0+ | Yes | Sets toolbar style: `.unified`, `.unifiedCompact`, `.expanded` |
|
|
23
|
+
| `windowStyle(_:)` | macOS 11.0+ | No | Supports `.hiddenTitleBar` for chromeless windows |
|
|
24
|
+
| `windowResizability(_:)` | macOS 13.0+ | No | Controls resize handle and green zoom button behavior |
|
|
25
|
+
| `defaultSize(width:height:)` | macOS 13.0+ | No | Initial frame size when user creates a new window |
|
|
26
|
+
| `defaultPosition(_:)` | macOS 13.0+ | No | Initial window position on screen |
|
|
27
|
+
| `windowIdealPlacement(_:)` | macOS 15.0+ | No | Closure with display geometry for precise window positioning |
|
|
28
|
+
| `menuBarExtraStyle(_:)` | macOS 13.0+ | Yes | Sets MenuBarExtra to `.menu` or `.window` style |
|
|
29
|
+
| `NavigationSplitView` | macOS 13.0+ | No | Columns always visible side-by-side on macOS; translucent sidebar |
|
|
30
|
+
| `Inspector` | macOS 14.0+ | No | Trailing-edge sidebar panel; resizable by dragging |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Toolbar Styles
|
|
35
|
+
|
|
36
|
+
### windowToolbarStyle (macOS-only)
|
|
37
|
+
|
|
38
|
+
Controls how the toolbar and title bar are displayed. Applied to a scene.
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
@main
|
|
42
|
+
struct MyApp: App {
|
|
43
|
+
var body: some Scene {
|
|
44
|
+
WindowGroup {
|
|
45
|
+
ContentView()
|
|
46
|
+
}
|
|
47
|
+
// Title bar and toolbar in a single row
|
|
48
|
+
.windowToolbarStyle(.unified)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Available styles:**
|
|
54
|
+
|
|
55
|
+
| Style | Description |
|
|
56
|
+
|-------|-------------|
|
|
57
|
+
| `.automatic` | System default |
|
|
58
|
+
| `.unified` | Title bar and toolbar in a single combined row |
|
|
59
|
+
| `.unifiedCompact` | Same as unified but with reduced vertical height |
|
|
60
|
+
| `.expanded` | Title bar displayed above the toolbar (more toolbar space) |
|
|
61
|
+
|
|
62
|
+
```swift
|
|
63
|
+
// Unified compact — minimal chrome
|
|
64
|
+
.windowToolbarStyle(.unifiedCompact)
|
|
65
|
+
|
|
66
|
+
// Expanded — title bar above toolbar
|
|
67
|
+
.windowToolbarStyle(.expanded)
|
|
68
|
+
|
|
69
|
+
// Unified with title hidden
|
|
70
|
+
.windowToolbarStyle(.unified(showsTitle: false))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Toolbar content
|
|
74
|
+
|
|
75
|
+
```swift
|
|
76
|
+
struct ContentView: View {
|
|
77
|
+
@State private var searchText = ""
|
|
78
|
+
|
|
79
|
+
var body: some View {
|
|
80
|
+
NavigationSplitView {
|
|
81
|
+
SidebarView()
|
|
82
|
+
} detail: {
|
|
83
|
+
DetailView()
|
|
84
|
+
}
|
|
85
|
+
.toolbar {
|
|
86
|
+
ToolbarItem(placement: .automatic) {
|
|
87
|
+
Button(action: addItem) {
|
|
88
|
+
Label("Add", systemImage: "plus")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
.searchable(text: $searchText, placement: .sidebar)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Window Style
|
|
100
|
+
|
|
101
|
+
### windowStyle
|
|
102
|
+
|
|
103
|
+
Set the visual style of a window. Use `.hiddenTitleBar` for chromeless, immersive windows.
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
// Standard title bar (default)
|
|
107
|
+
WindowGroup {
|
|
108
|
+
ContentView()
|
|
109
|
+
}
|
|
110
|
+
.windowStyle(.titleBar)
|
|
111
|
+
|
|
112
|
+
// Hidden title bar — chromeless window
|
|
113
|
+
WindowGroup {
|
|
114
|
+
ContentView()
|
|
115
|
+
}
|
|
116
|
+
.windowStyle(.hiddenTitleBar)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **Use case:** `.hiddenTitleBar` is useful for media players, custom-chrome apps, or immersive experiences where the standard title bar is unwanted.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Window Sizing
|
|
124
|
+
|
|
125
|
+
### windowResizability, defaultSize, defaultPosition
|
|
126
|
+
|
|
127
|
+
These modifiers work together to configure window sizing and placement:
|
|
128
|
+
|
|
129
|
+
```swift
|
|
130
|
+
WindowGroup {
|
|
131
|
+
ContentView()
|
|
132
|
+
.frame(minWidth: 600, minHeight: 400)
|
|
133
|
+
}
|
|
134
|
+
.defaultSize(width: 900, height: 600)
|
|
135
|
+
.defaultPosition(.center)
|
|
136
|
+
.windowResizability(.contentMinSize)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**`windowResizability` options:**
|
|
140
|
+
|
|
141
|
+
| Value | Behavior |
|
|
142
|
+
|-------|----------|
|
|
143
|
+
| `.automatic` | System decides resize behavior |
|
|
144
|
+
| `.contentSize` | Fixed to content size; no user resize; zoom button disabled |
|
|
145
|
+
| `.contentMinSize` | Resizable with minimum based on content's `minWidth`/`minHeight` |
|
|
146
|
+
|
|
147
|
+
**`defaultPosition` options:** `.center`, `.topLeading`, `.top`, `.topTrailing`, `.leading`, `.trailing`, `.bottomLeading`, `.bottom`, `.bottomTrailing`
|
|
148
|
+
|
|
149
|
+
**Guidelines:**
|
|
150
|
+
- Set `minWidth`/`minHeight` via `.frame()` on content, enforce with `.contentMinSize`
|
|
151
|
+
- Use `.defaultSize()` for initial dimensions (larger than minimums)
|
|
152
|
+
- `defaultSize` also accepts `CGSize`
|
|
153
|
+
|
|
154
|
+
### windowIdealPlacement (macOS 15.0+)
|
|
155
|
+
|
|
156
|
+
For precise programmatic positioning, use a closure with display geometry:
|
|
157
|
+
|
|
158
|
+
```swift
|
|
159
|
+
.windowIdealPlacement { context in
|
|
160
|
+
let screen = context.defaultDisplay.visibleArea
|
|
161
|
+
return WindowPlacement(x: screen.midX, y: screen.midY,
|
|
162
|
+
width: screen.width / 2, height: screen.height)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## MenuBarExtra Style (macOS-only)
|
|
169
|
+
|
|
170
|
+
Choose between dropdown menu and popover panel for `MenuBarExtra`.
|
|
171
|
+
|
|
172
|
+
```swift
|
|
173
|
+
// Dropdown menu (default)
|
|
174
|
+
MenuBarExtra("Status", systemImage: "chart.bar") {
|
|
175
|
+
Button("Action") { /* ... */ }
|
|
176
|
+
}
|
|
177
|
+
.menuBarExtraStyle(.menu)
|
|
178
|
+
|
|
179
|
+
// Popover panel with custom SwiftUI content
|
|
180
|
+
MenuBarExtra("Status", systemImage: "chart.bar") {
|
|
181
|
+
DashboardView()
|
|
182
|
+
}
|
|
183
|
+
.menuBarExtraStyle(.window)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Navigation Layout (macOS behavior)
|
|
189
|
+
|
|
190
|
+
### NavigationSplitView
|
|
191
|
+
|
|
192
|
+
On macOS, `NavigationSplitView` displays columns side-by-side (never overlaid). The sidebar gets a translucent material background. Columns support variable-width resizing by the user.
|
|
193
|
+
|
|
194
|
+
```swift
|
|
195
|
+
NavigationSplitView {
|
|
196
|
+
List(items, selection: $selectedId) { item in
|
|
197
|
+
Text(item.name)
|
|
198
|
+
}
|
|
199
|
+
.navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 300)
|
|
200
|
+
} detail: {
|
|
201
|
+
DetailView(id: selectedId)
|
|
202
|
+
}
|
|
203
|
+
.navigationSplitViewStyle(.balanced)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Use the three-column variant (`sidebar` / `content` / `detail`) for master-detail-detail layouts. Customize column widths with `.navigationSplitViewColumnWidth(min:ideal:max:)`.
|
|
207
|
+
|
|
208
|
+
### Inspector (macOS 14.0+)
|
|
209
|
+
|
|
210
|
+
A trailing-edge panel for supplementary information. On macOS, it appears as a sidebar-style panel that can be resized by dragging its edge.
|
|
211
|
+
|
|
212
|
+
```swift
|
|
213
|
+
struct ContentView: View {
|
|
214
|
+
@State private var showInspector = false
|
|
215
|
+
|
|
216
|
+
var body: some View {
|
|
217
|
+
MainContent()
|
|
218
|
+
.inspector(isPresented: $showInspector) {
|
|
219
|
+
InspectorView()
|
|
220
|
+
.inspectorColumnWidth(min: 200, ideal: 250, max: 400)
|
|
221
|
+
}
|
|
222
|
+
.toolbar {
|
|
223
|
+
ToolbarItem {
|
|
224
|
+
Button {
|
|
225
|
+
showInspector.toggle()
|
|
226
|
+
} label: {
|
|
227
|
+
Label("Inspector", systemImage: "info.circle")
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Commands & Keyboard
|
|
238
|
+
|
|
239
|
+
### Commands, CommandGroup, CommandMenu
|
|
240
|
+
|
|
241
|
+
Define menu bar commands. On macOS, these populate the menu bar directly. On iOS, they create key commands.
|
|
242
|
+
|
|
243
|
+
```swift
|
|
244
|
+
.commands {
|
|
245
|
+
CommandMenu("Tools") {
|
|
246
|
+
Button("Run Analysis") { /* ... */ }
|
|
247
|
+
.keyboardShortcut("r", modifiers: [.command, .shift])
|
|
248
|
+
}
|
|
249
|
+
CommandGroup(after: .newItem) {
|
|
250
|
+
Button("New From Template...") { /* ... */ }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**`CommandGroup` placement options:** `.replacing(_:)` replaces a system group, `.before(_:)` / `.after(_:)` inserts adjacent to it. Common placements: `.newItem`, `.saveItem`, `.help`, `.toolbar`, `.sidebar`.
|
|
256
|
+
|
|
257
|
+
### KeyboardShortcut
|
|
258
|
+
|
|
259
|
+
On macOS, shortcuts are displayed alongside menu items and in button tooltips on hover.
|
|
260
|
+
|
|
261
|
+
```swift
|
|
262
|
+
Button("Save") {
|
|
263
|
+
save()
|
|
264
|
+
}
|
|
265
|
+
.keyboardShortcut("s", modifiers: .command)
|
|
266
|
+
|
|
267
|
+
Button("Delete") {
|
|
268
|
+
delete()
|
|
269
|
+
}
|
|
270
|
+
.keyboardShortcut(.delete, modifiers: .command)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### openWindow
|
|
274
|
+
|
|
275
|
+
Programmatically open a window. If the target window is already open, brings it to the front.
|
|
276
|
+
|
|
277
|
+
```swift
|
|
278
|
+
struct ToolbarActions: View {
|
|
279
|
+
@Environment(\.openWindow) private var openWindow
|
|
280
|
+
|
|
281
|
+
var body: some View {
|
|
282
|
+
Button("Connection Doctor") {
|
|
283
|
+
openWindow(id: "connection-doctor")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Button("Show Message") {
|
|
287
|
+
openWindow(value: message.id) // Type-matched to WindowGroup
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Best Practices
|
|
296
|
+
|
|
297
|
+
- **Use `.unified` or `.unifiedCompact`** for most apps — `.expanded` only when you need many toolbar items
|
|
298
|
+
- **Set min frame sizes on content** and use `.windowResizability(.contentMinSize)` to enforce them
|
|
299
|
+
- **Always provide `defaultSize`** so new windows start at a reasonable size
|
|
300
|
+
- **Use `NavigationSplitView`** for sidebar navigation — not `HSplitView`
|
|
301
|
+
- **Use `Inspector`** for supplementary panels — it integrates with the toolbar automatically
|
|
302
|
+
- **Define `Commands`** for all repeatable actions — users expect keyboard shortcuts on macOS
|
|
303
|
+
- **Use `#if os(macOS)`** to wrap macOS-only window configuration in multiplatform projects
|