3.9 — macOS design considerations
Opening scenario
You ship your iPhone app to macOS using Mac Catalyst. It launches. It’s a single 390-pt-wide column floating in the middle of a 27“ display. No menu bar items. No keyboard shortcuts. The right-click does nothing. The window resizes but the layout just stretches awkwardly. App Store reviews: “feels like a phone app duct-taped to my Mac.” 2 stars.
macOS is not iPad scaled up, and certainly not iPhone scaled up. It is a different platform with different metaphors: pointer (not finger), keyboard-first, multi-window, menu bar, contextual right-click. This chapter covers the design patterns that make a SwiftUI app feel Mac-native rather than ported.
| Concept | iOS | macOS |
|---|---|---|
| Primary nav | Tab bar / NavigationStack | Sidebar (NavigationSplitView) |
| Action invocation | Tap (44pt target) | Click + keyboard shortcut + menu bar |
| Context actions | Long-press menu | Right-click menu (always) |
| Discoverability | On-screen | Menu bar (File, Edit, View, Window…) |
| Multitasking | Stage Manager / Split View (recent) | Multi-window from day one |
| Input | Touch | Pointer, keyboard, trackpad gestures |
| Window | Full-screen by default | Resizable, draggable, multiple instances |
Concept → Why → How → Code
The three-pane layout
The canonical Mac app uses NavigationSplitView with three columns:
NavigationSplitView {
SidebarView() // categories / inbox / sections
} content: {
ListView() // items in selected category
} detail: {
DetailView() // selected item content
}
Mail, Notes, Reminders, Finder, Messages, Music, Podcasts — all built on this template. Users expect the columns. Three-pane is the Mac equivalent of iPhone’s tab bar.
On smaller windows, the sidebar collapses into a button; on full width, all three columns show. SwiftUI handles the collapse automatically.
Toolbar — the Mac action surface
The toolbar at the top of a Mac window holds frequent actions. Use ToolbarItem:
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("New") { create() }
.keyboardShortcut("n", modifiers: .command)
}
ToolbarItem(placement: .secondaryAction) {
Button { share() } label: {
Image(systemName: "square.and.arrow.up")
}
}
ToolbarItem(placement: .navigation) {
Button { goBack() } label: {
Image(systemName: "chevron.left")
}
}
}
Toolbar items show as icon-only by default; the user can right-click → Customize Toolbar → choose icon + text or icon-only. SwiftUI handles this for free.
Menu bar — discoverability and keyboard
Every Mac app gets a menu bar with App, File, Edit, View, Window, Help by default. Add custom commands:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("Notes") {
Button("New Note") { newNote() }
.keyboardShortcut("n", modifiers: .command)
Button("Pin") { togglePin() }
.keyboardShortcut("p", modifiers: [.command, .shift])
Divider()
Button("Export…") { showExport() }
}
// Replace stock menus
CommandGroup(replacing: .newItem) {
Button("New Note") { newNote() }
.keyboardShortcut("n")
}
}
}
}
Every command should:
- Live in the menu bar (discoverability)
- Have a keyboard shortcut (efficiency)
- Optionally appear in the toolbar (frequent use)
The user can find a command three ways. Excellent Mac apps make every action discoverable from at least the menu.
Multi-window — first-class citizen
iPhone has one window. Mac has many. Design assuming the user has 5 windows of your app open.
@main
struct MyApp: App {
var body: some Scene {
// Main document window
WindowGroup(for: Note.self) { $note in
NoteEditorView(note: $note)
}
// Settings (Cmd+,)
Settings {
SettingsView()
}
// Menu bar extra (status icon, like Dropbox)
MenuBarExtra("Quick Note", systemImage: "note.text") {
QuickNoteView()
}
}
}
WindowGroup(for:) lets users open multiple windows, each tied to a different model — like opening multiple Notes in Notes app. Use @Environment(\.openWindow) to programmatically open new windows:
struct ContentView: View {
@Environment(\.openWindow) var openWindow
var body: some View {
Button("New Window") {
openWindow(value: Note(title: "Untitled"))
}
}
}
Right-click context menus
iOS has long-press; Mac has right-click. The two should map. SwiftUI handles both with .contextMenu:
Text(item.title)
.contextMenu {
Button("Open") { open(item) }
Button("Rename") { rename(item) }
Divider()
Button("Delete", role: .destructive) { delete(item) }
}
On Mac, the menu appears at the cursor on right-click. On iPhone, the same menu appears with a long-press. Write once, ship both.
Pointer affordances
The pointer is precise (1pt resolution vs 44pt fingertip). You can ship UI that requires precise hits — but you must signal interactivity. Use .onHover to swap cursor or change visual state:
struct LinkRow: View {
@State private var hovering = false
var body: some View {
HStack {
Text("Open Settings")
Spacer()
Image(systemName: "chevron.right")
}
.padding()
.background(hovering ? Color.secondary.opacity(0.1) : Color.clear)
.onHover { hovering = $0 }
}
}
Pointer changes (resize cursor, link cursor) come automatically with system controls. For custom interactive areas, set cursor via NSCursor (requires AppKit interop) or use pointerStyle() (macOS 15+):
.pointerStyle(.link) // ← hand cursor
.pointerStyle(.text) // I-beam
.pointerStyle(.grabIdle) // open hand
Window resizing
Mac windows resize. Your layout must handle every aspect ratio between minimum and maximum. Three patterns:
// Set a minimum window size
WindowGroup {
ContentView()
.frame(minWidth: 600, minHeight: 400)
}
// Or fix the size (rarely correct — only for utility windows)
.windowResizability(.contentSize)
// Use NavigationSplitView columns with widths
NavigationSplitView(
columnVisibility: $columnVisibility,
sidebar: { Sidebar().navigationSplitViewColumnWidth(min: 180, ideal: 220) },
content: { Content().navigationSplitViewColumnWidth(min: 280) },
detail: { Detail() }
)
Resize the window during dev. Verify text doesn’t truncate, images don’t squish, lists scroll cleanly.
Mac vs iOS — the conversion mistakes
Common iOS-thinking ported wrong:
| iOS pattern | Mac equivalent |
|---|---|
| Floating action button | Toolbar primary action |
| Tab bar at bottom | Sidebar with sections |
| Bottom sheet | Sheet (top) or window |
| Pull-to-refresh | Cmd+R + menu item “View → Reload” |
| Swipe to delete | Right-click → Delete + Delete key shortcut |
| Hamburger menu | Sidebar (always visible by default) |
| Large header that collapses | Standard title (no collapse) |
| “Done” button top-right | Cmd+W to close, Cmd+S to save |
Designers who design “iOS then port to Mac” produce these antipatterns. Designers who design for the platform never do.
Toolbar customization
Users can customize their Mac apps’ toolbars (right-click → Customize Toolbar). Make this work by giving each ToolbarItem an id:
.toolbar(id: "main") {
ToolbarItem(id: "new", placement: .primaryAction) {
Button("New") { newDoc() }
}
ToolbarItem(id: "share", placement: .secondaryAction) {
ShareLink(item: url)
}
}
.toolbarRole(.editor) // Mac-style toolbar with title
Settings window
Cmd+, opens settings on Mac. SwiftUI:
Settings {
TabView {
GeneralSettingsView()
.tabItem { Label("General", systemImage: "gear") }
AdvancedSettingsView()
.tabItem { Label("Advanced", systemImage: "wrench") }
}
.frame(width: 500, height: 350)
}
Tabbed settings is the Mac convention (matches Finder, Safari, Mail). On iOS, settings is a navigation push from a list.
MenuBarExtra — the status icon
For utilities (clipboard manager, weather, timer), use MenuBarExtra:
MenuBarExtra("Pomodoro", systemImage: "timer") {
PomodoroPopover()
}
.menuBarExtraStyle(.window) // popover vs menu
Excellent for ambient apps that don’t need a main window.
Mac Catalyst vs SwiftUI Multiplatform vs separate AppKit
Three ways to ship a Mac app today:
- SwiftUI multiplatform target (recommended for new apps) — one target,
#if os(macOS)for platform-specific bits, runs natively on both - Mac Catalyst — UIKit-based iPad app, recompiled for Mac with Mac chrome. Decent for ports; never quite feels native
- Separate AppKit target — for legacy or extremely platform-specific apps (Logic Pro, Final Cut)
For 95% of new apps: SwiftUI multiplatform. Mac Catalyst only if you’re porting an existing iPad app with deep UIKit.
In the wild
- Things 3 (Cultured Code) — gold standard SwiftUI Mac app, three-pane, deep keyboard, great toolbar, multi-window
- Notion’s Mac app — was an Electron port for years; SwiftUI rewrite shipped 2023 and got better reviews immediately
- Linear’s Mac app — minimal, keyboard-first, near-perfect Mac feel despite being React under the hood (Catalyst-ish)
- Apple Music on Mac — uses three-pane sidebar and a fully customizable toolbar — reference implementation
- Slack on Mac — Electron, no MenuBarExtra, no keyboard customization — the textbook bad Mac citizen
Common misconceptions
- “Mac is dying, iPad is the future.” Mac shipped record units in 2024 and is the primary dev machine for ~80% of senior software engineers. Pro market is huge.
- “Mac Catalyst is good enough.” It works, but Catalyst apps consistently score 0.5-1 star lower in App Store reviews. Native SwiftUI is the better path.
- “Users don’t customize Mac toolbars.” Pro users absolutely do. Not enabling customization signals an amateur app.
- “Mac doesn’t need touch-friendly tap targets.” True — pointer can hit 1pt targets. But don’t go below 16-20pt for buttons; usability research shows >16pt is meaningfully easier even with pointer.
- “Menu bar is bloated, just put everything in toolbar.” Wrong. Menu bar is the index of every action; toolbar is the frequent subset. Users discover via menu bar.
Seasoned engineer’s take
Building Mac apps is the highest-leverage iOS skill in 2025. Apple Silicon, Vision Pro, and the SwiftUI maturity curve mean Mac apps are easier to build than ever, and the App Store competition is much thinner than iOS. A polished Mac app gets featured, gets press, and converts well.
The single decision that makes or breaks Mac feel: do you embrace the platform metaphors (multi-window, menu bar, right-click, keyboard) or do you fight them with iPhone idioms? Embrace = 5 stars. Fight = 2 stars.
Spend time using Apple’s own Mac apps for a week — Notes, Mail, Finder, Music, Reminders. Watch how they handle window resize, sidebar collapse, toolbar customization, keyboard navigation. That’s your spec.
TIP: Build the entire app keyboard-only first. No mouse for a day. You’ll discover every missing shortcut and every place users get stuck. Then add menu items.
WARNING: Don’t ship a Mac app without a
Help → Keyboard Shortcutsmenu item. Users expect a way to discover all shortcuts. SwiftUI lists them automatically in the Help menu’s search box — make sure each command has a.keyboardShortcut(...)so they show up.
Interview corner
Junior-level: “How is designing for Mac different from iOS?”
Pointer (precise) vs touch (44pt). Keyboard shortcuts and menu bar (every command discoverable). Multi-window vs single window. Right-click for context. Resizable windows with adaptive layouts. Sidebar instead of tab bar.
Mid-level: “How do you support multiple windows in a SwiftUI Mac app?”
WindowGroup(for: Model.self) for document-style multi-window. @Environment(\.openWindow) to programmatically open. Settings for the Cmd+, window. MenuBarExtra for status-bar utilities. Each window has its own state; shared state goes in a singleton model accessed via @Environment or Observable.
Senior-level: “You’re porting a complex iOS app to Mac. Catalyst, multiplatform SwiftUI, or AppKit — and why?”
Depends. Pure SwiftUI iOS code → SwiftUI multiplatform, one target with #if os(macOS) for platform features (toolbar, menu bar, multi-window). Heavy UIKit codebase with little engineering bandwidth → Catalyst as a stopgap (ship faster, schedule SwiftUI migration). Legacy product requiring Mac-specific features unavailable in Catalyst (deep file system access, services menu, advanced print support) → AppKit. Decision matrix: engineering cost vs target feel quality.
Red flag in candidates: Shipping a Mac app as “iOS app, but in a window.” Means they didn’t read HIG.
Lab preview
You’ll add macOS support to a multiplatform app in Phase 5’s labs — but the Mac patterns you learn here apply to every later iOS/Mac project.