304 lines
8.6 KiB
Markdown
304 lines
8.6 KiB
Markdown
# macOS Window & Toolbar Styling Reference
|
|
|
|
> Window configuration, toolbar styles, sizing, positioning, and navigation patterns specific to macOS SwiftUI apps.
|
|
|
|
## Table of Contents
|
|
|
|
- [Quick Lookup Table](#quick-lookup-table)
|
|
- [Toolbar Styles](#toolbar-styles)
|
|
- [Window Style](#window-style)
|
|
- [Window Sizing](#window-sizing)
|
|
- [MenuBarExtra Style (macOS-only)](#menubarextra-style-macos-only)
|
|
- [Navigation Layout (macOS behavior)](#navigation-layout-macos-behavior)
|
|
- [Commands & Keyboard](#commands--keyboard)
|
|
- [Best Practices](#best-practices)
|
|
|
|
---
|
|
|
|
## Quick Lookup Table
|
|
|
|
| API | Availability | macOS-Only? | Usage |
|
|
|-----|-------------|:-----------:|-------|
|
|
| `windowToolbarStyle(_:)` | macOS 11.0+ | Yes | Sets toolbar style: `.unified`, `.unifiedCompact`, `.expanded` |
|
|
| `windowStyle(_:)` | macOS 11.0+ | No | Supports `.hiddenTitleBar` for chromeless windows |
|
|
| `windowResizability(_:)` | macOS 13.0+ | No | Controls resize handle and green zoom button behavior |
|
|
| `defaultSize(width:height:)` | macOS 13.0+ | No | Initial frame size when user creates a new window |
|
|
| `defaultPosition(_:)` | macOS 13.0+ | No | Initial window position on screen |
|
|
| `windowIdealPlacement(_:)` | macOS 15.0+ | No | Closure with display geometry for precise window positioning |
|
|
| `menuBarExtraStyle(_:)` | macOS 13.0+ | Yes | Sets MenuBarExtra to `.menu` or `.window` style |
|
|
| `NavigationSplitView` | macOS 13.0+ | No | Columns always visible side-by-side on macOS; translucent sidebar |
|
|
| `Inspector` | macOS 14.0+ | No | Trailing-edge sidebar panel; resizable by dragging |
|
|
|
|
---
|
|
|
|
## Toolbar Styles
|
|
|
|
### windowToolbarStyle (macOS-only)
|
|
|
|
Controls how the toolbar and title bar are displayed. Applied to a scene.
|
|
|
|
```swift
|
|
@main
|
|
struct MyApp: App {
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
}
|
|
// Title bar and toolbar in a single row
|
|
.windowToolbarStyle(.unified)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Available styles:**
|
|
|
|
| Style | Description |
|
|
|-------|-------------|
|
|
| `.automatic` | System default |
|
|
| `.unified` | Title bar and toolbar in a single combined row |
|
|
| `.unifiedCompact` | Same as unified but with reduced vertical height |
|
|
| `.expanded` | Title bar displayed above the toolbar (more toolbar space) |
|
|
|
|
```swift
|
|
// Unified compact — minimal chrome
|
|
.windowToolbarStyle(.unifiedCompact)
|
|
|
|
// Expanded — title bar above toolbar
|
|
.windowToolbarStyle(.expanded)
|
|
|
|
// Unified with title hidden
|
|
.windowToolbarStyle(.unified(showsTitle: false))
|
|
```
|
|
|
|
### Toolbar content
|
|
|
|
```swift
|
|
struct ContentView: View {
|
|
@State private var searchText = ""
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
SidebarView()
|
|
} detail: {
|
|
DetailView()
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .automatic) {
|
|
Button(action: addItem) {
|
|
Label("Add", systemImage: "plus")
|
|
}
|
|
}
|
|
}
|
|
.searchable(text: $searchText, placement: .sidebar)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Window Style
|
|
|
|
### windowStyle
|
|
|
|
Set the visual style of a window. Use `.hiddenTitleBar` for chromeless, immersive windows.
|
|
|
|
```swift
|
|
// Standard title bar (default)
|
|
WindowGroup {
|
|
ContentView()
|
|
}
|
|
.windowStyle(.titleBar)
|
|
|
|
// Hidden title bar — chromeless window
|
|
WindowGroup {
|
|
ContentView()
|
|
}
|
|
.windowStyle(.hiddenTitleBar)
|
|
```
|
|
|
|
> **Use case:** `.hiddenTitleBar` is useful for media players, custom-chrome apps, or immersive experiences where the standard title bar is unwanted.
|
|
|
|
---
|
|
|
|
## Window Sizing
|
|
|
|
### windowResizability, defaultSize, defaultPosition
|
|
|
|
These modifiers work together to configure window sizing and placement:
|
|
|
|
```swift
|
|
WindowGroup {
|
|
ContentView()
|
|
.frame(minWidth: 600, minHeight: 400)
|
|
}
|
|
.defaultSize(width: 900, height: 600)
|
|
.defaultPosition(.center)
|
|
.windowResizability(.contentMinSize)
|
|
```
|
|
|
|
**`windowResizability` options:**
|
|
|
|
| Value | Behavior |
|
|
|-------|----------|
|
|
| `.automatic` | System decides resize behavior |
|
|
| `.contentSize` | Fixed to content size; no user resize; zoom button disabled |
|
|
| `.contentMinSize` | Resizable with minimum based on content's `minWidth`/`minHeight` |
|
|
|
|
**`defaultPosition` options:** `.center`, `.topLeading`, `.top`, `.topTrailing`, `.leading`, `.trailing`, `.bottomLeading`, `.bottom`, `.bottomTrailing`
|
|
|
|
**Guidelines:**
|
|
- Set `minWidth`/`minHeight` via `.frame()` on content, enforce with `.contentMinSize`
|
|
- Use `.defaultSize()` for initial dimensions (larger than minimums)
|
|
- `defaultSize` also accepts `CGSize`
|
|
|
|
### windowIdealPlacement (macOS 15.0+)
|
|
|
|
For precise programmatic positioning, use a closure with display geometry:
|
|
|
|
```swift
|
|
.windowIdealPlacement { context in
|
|
let screen = context.defaultDisplay.visibleArea
|
|
return WindowPlacement(x: screen.midX, y: screen.midY,
|
|
width: screen.width / 2, height: screen.height)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## MenuBarExtra Style (macOS-only)
|
|
|
|
Choose between dropdown menu and popover panel for `MenuBarExtra`.
|
|
|
|
```swift
|
|
// Dropdown menu (default)
|
|
MenuBarExtra("Status", systemImage: "chart.bar") {
|
|
Button("Action") { /* ... */ }
|
|
}
|
|
.menuBarExtraStyle(.menu)
|
|
|
|
// Popover panel with custom SwiftUI content
|
|
MenuBarExtra("Status", systemImage: "chart.bar") {
|
|
DashboardView()
|
|
}
|
|
.menuBarExtraStyle(.window)
|
|
```
|
|
|
|
---
|
|
|
|
## Navigation Layout (macOS behavior)
|
|
|
|
### NavigationSplitView
|
|
|
|
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.
|
|
|
|
```swift
|
|
NavigationSplitView {
|
|
List(items, selection: $selectedId) { item in
|
|
Text(item.name)
|
|
}
|
|
.navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 300)
|
|
} detail: {
|
|
DetailView(id: selectedId)
|
|
}
|
|
.navigationSplitViewStyle(.balanced)
|
|
```
|
|
|
|
Use the three-column variant (`sidebar` / `content` / `detail`) for master-detail-detail layouts. Customize column widths with `.navigationSplitViewColumnWidth(min:ideal:max:)`.
|
|
|
|
### Inspector (macOS 14.0+)
|
|
|
|
A trailing-edge panel for supplementary information. On macOS, it appears as a sidebar-style panel that can be resized by dragging its edge.
|
|
|
|
```swift
|
|
struct ContentView: View {
|
|
@State private var showInspector = false
|
|
|
|
var body: some View {
|
|
MainContent()
|
|
.inspector(isPresented: $showInspector) {
|
|
InspectorView()
|
|
.inspectorColumnWidth(min: 200, ideal: 250, max: 400)
|
|
}
|
|
.toolbar {
|
|
ToolbarItem {
|
|
Button {
|
|
showInspector.toggle()
|
|
} label: {
|
|
Label("Inspector", systemImage: "info.circle")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Commands & Keyboard
|
|
|
|
### Commands, CommandGroup, CommandMenu
|
|
|
|
Define menu bar commands. On macOS, these populate the menu bar directly. On iOS, they create key commands.
|
|
|
|
```swift
|
|
.commands {
|
|
CommandMenu("Tools") {
|
|
Button("Run Analysis") { /* ... */ }
|
|
.keyboardShortcut("r", modifiers: [.command, .shift])
|
|
}
|
|
CommandGroup(after: .newItem) {
|
|
Button("New From Template...") { /* ... */ }
|
|
}
|
|
}
|
|
```
|
|
|
|
**`CommandGroup` placement options:** `.replacing(_:)` replaces a system group, `.before(_:)` / `.after(_:)` inserts adjacent to it. Common placements: `.newItem`, `.saveItem`, `.help`, `.toolbar`, `.sidebar`.
|
|
|
|
### KeyboardShortcut
|
|
|
|
On macOS, shortcuts are displayed alongside menu items and in button tooltips on hover.
|
|
|
|
```swift
|
|
Button("Save") {
|
|
save()
|
|
}
|
|
.keyboardShortcut("s", modifiers: .command)
|
|
|
|
Button("Delete") {
|
|
delete()
|
|
}
|
|
.keyboardShortcut(.delete, modifiers: .command)
|
|
```
|
|
|
|
### openWindow
|
|
|
|
Programmatically open a window. If the target window is already open, brings it to the front.
|
|
|
|
```swift
|
|
struct ToolbarActions: View {
|
|
@Environment(\.openWindow) private var openWindow
|
|
|
|
var body: some View {
|
|
Button("Connection Doctor") {
|
|
openWindow(id: "connection-doctor")
|
|
}
|
|
|
|
Button("Show Message") {
|
|
openWindow(value: message.id) // Type-matched to WindowGroup
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
- **Use `.unified` or `.unifiedCompact`** for most apps — `.expanded` only when you need many toolbar items
|
|
- **Set min frame sizes on content** and use `.windowResizability(.contentMinSize)` to enforce them
|
|
- **Always provide `defaultSize`** so new windows start at a reasonable size
|
|
- **Use `NavigationSplitView`** for sidebar navigation — not `HSplitView`
|
|
- **Use `Inspector`** for supplementary panels — it integrates with the toolbar automatically
|
|
- **Define `Commands`** for all repeatable actions — users expect keyboard shortcuts on macOS
|
|
- **Use `#if os(macOS)`** to wrap macOS-only window configuration in multiplatform projects
|