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.

ConceptiOSmacOS
Primary navTab bar / NavigationStackSidebar (NavigationSplitView)
Action invocationTap (44pt target)Click + keyboard shortcut + menu bar
Context actionsLong-press menuRight-click menu (always)
DiscoverabilityOn-screenMenu bar (File, Edit, View, Window…)
MultitaskingStage Manager / Split View (recent)Multi-window from day one
InputTouchPointer, keyboard, trackpad gestures
WindowFull-screen by defaultResizable, 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.

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:

  1. Live in the menu bar (discoverability)
  2. Have a keyboard shortcut (efficiency)
  3. 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 patternMac equivalent
Floating action buttonToolbar primary action
Tab bar at bottomSidebar with sections
Bottom sheetSheet (top) or window
Pull-to-refreshCmd+R + menu item “View → Reload”
Swipe to deleteRight-click → Delete + Delete key shortcut
Hamburger menuSidebar (always visible by default)
Large header that collapsesStandard title (no collapse)
“Done” button top-rightCmd+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.

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:

  1. SwiftUI multiplatform target (recommended for new apps) — one target, #if os(macOS) for platform-specific bits, runs natively on both
  2. Mac Catalyst — UIKit-based iPad app, recompiled for Mac with Mac chrome. Decent for ports; never quite feels native
  3. 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

  1. “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.
  2. “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.
  3. “Users don’t customize Mac toolbars.” Pro users absolutely do. Not enabling customization signals an amateur app.
  4. “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.
  5. “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 Shortcuts menu 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.


Next: 3.10 — Color psychology & palette design