macOS Development Patterns
Comprehensive guide to macOS Tahoe development, window management, menu bars, document-based apps, and macOS-specific SwiftUI patterns.
Prerequisites
-
macOS Tahoe (macOS 26) or later
-
Xcode 26+
Window Management
Window Styles
import SwiftUI
@main struct MyMacApp: App { var body: some Scene { // Standard window WindowGroup { ContentView() } .windowStyle(.automatic) // Default
// Hideable title bar (content extends to top)
WindowGroup("Editor", id: "editor") {
EditorView()
}
.windowStyle(.hiddenTitleBar)
// Plain window (no chrome)
WindowGroup("Floating", id: "floating") {
FloatingView()
}
.windowStyle(.plain)
}
}
Window Size and Position
WindowGroup { ContentView() } // Default size .defaultSize(width: 800, height: 600)
// Size constraints .windowResizability(.contentSize) // Fit content .windowResizability(.contentMinSize) // Min = content, resizable larger .windowResizability(.automatic) // System decides
// Fixed size window .windowResizability(.contentSize) .frame(width: 400, height: 300)
// Position .defaultPosition(.center) .defaultPosition(.topLeading) .defaultPosition(UnitPoint(x: 0.75, y: 0.25))
Multiple Windows
@main struct MultiWindowApp: App { @Environment(.openWindow) private var openWindow
var body: some Scene {
// Main window
WindowGroup {
MainView()
.toolbar {
Button("New Editor") {
openWindow(id: "editor")
}
}
}
.commands {
CommandGroup(after: .newItem) {
Button("New Editor Window") {
openWindow(id: "editor")
}
.keyboardShortcut("e", modifiers: [.command, .shift])
}
}
// Secondary window type
WindowGroup("Editor", id: "editor") {
EditorView()
}
.defaultSize(width: 600, height: 400)
// Single instance window
Window("Settings", id: "settings") {
SettingsView()
}
.keyboardShortcut(",", modifiers: .command)
.defaultSize(width: 500, height: 400)
}
}
Window with Data
// Define window value type struct DocumentInfo: Codable, Hashable { let id: UUID let title: String }
@main struct DocumentApp: App { var body: some Scene { WindowGroup(for: DocumentInfo.self) { $document in if let document { DocumentView(info: document) } } } }
// Open with specific data struct ContentView: View { @Environment(.openWindow) private var openWindow
var body: some View {
Button("Open Document") {
openWindow(value: DocumentInfo(id: UUID(), title: "New Doc"))
}
}
}
NSWindow Integration
import AppKit import SwiftUI
struct WindowAccessor: NSViewRepresentable { let callback: (NSWindow?) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
callback(view.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
// Usage struct ContentView: View { @State private var window: NSWindow?
var body: some View {
Text("Hello")
.background(WindowAccessor { window in
self.window = window
// Customize window
window?.titlebarAppearsTransparent = true
window?.titleVisibility = .hidden
window?.styleMask.insert(.fullSizeContentView)
window?.isMovableByWindowBackground = true
})
}
}
Menu Bar
App Menu Customization
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .commands { // Replace existing menu group CommandGroup(replacing: .newItem) { Button("New Document") { // Create new document } .keyboardShortcut("n")
Button("New from Template...") {
// Show template picker
}
.keyboardShortcut("n", modifiers: [.command, .shift])
}
// Add to existing menu
CommandGroup(after: .sidebar) {
Divider()
Button("Toggle Inspector") {
// Toggle inspector
}
.keyboardShortcut("i", modifiers: [.command, .option])
}
// Custom menu
CommandMenu("Canvas") {
Button("Zoom In") { }
.keyboardShortcut("+")
Button("Zoom Out") { }
.keyboardShortcut("-")
Divider()
Button("Fit to Window") { }
.keyboardShortcut("0")
}
}
}
}
Context Menus (Right-Click)
struct ItemView: View { let item: Item @State private var isRenaming = false
var body: some View {
Text(item.title)
.contextMenu {
Button("Open") {
// Open action
}
Button("Open in New Window") {
// Open in new window
}
Divider()
Button("Rename") {
isRenaming = true
}
Button("Duplicate") {
// Duplicate action
}
Divider()
Button("Delete", role: .destructive) {
// Delete action
}
}
}
}
Menu Bar Extra (Status Bar Items)
@main struct StatusBarApp: App { var body: some Scene { // Optional main window WindowGroup { ContentView() }
// Status bar item
MenuBarExtra("My App", systemImage: "star.fill") {
Button("Show Dashboard") {
// Open main window
}
.keyboardShortcut("d")
Divider()
Menu("Recent Items") {
ForEach(recentItems) { item in
Button(item.name) {
// Open item
}
}
}
Divider()
Button("Preferences...") {
// Open preferences
}
.keyboardShortcut(",")
Button("Quit") {
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("q")
}
}
@State private var recentItems: [RecentItem] = []
}
// Custom status bar view struct StatusBarApp2: App { var body: some Scene { MenuBarExtra { StatusBarPopover() } label: { HStack(spacing: 4) { Image(systemName: "cpu") Text("45%") .font(.caption) } } .menuBarExtraStyle(.window) // Popover style } }
struct StatusBarPopover: View { var body: some View { VStack(spacing: 16) { Text("System Status") .font(.headline)
// Status content
StatusRow(title: "CPU", value: "45%")
StatusRow(title: "Memory", value: "8.2 GB")
StatusRow(title: "Storage", value: "234 GB")
Divider()
Button("Open Activity Monitor") {
// Open Activity Monitor
}
}
.padding()
.frame(width: 200)
}
}
Document-Based Apps
Document Type Definition
import SwiftUI import UniformTypeIdentifiers
// Define document type extension UTType { static var myDocument: UTType { UTType(exportedAs: "com.mycompany.mydocument") } }
// Document model struct MyDocument: FileDocument { static var readableContentTypes: [UTType] { [.myDocument, .plainText] } static var writableContentTypes: [UTType] { [.myDocument] }
var content: String
init(content: String = "") {
self.content = content
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8) else {
throw CocoaError(.fileReadCorruptFile)
}
content = string
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = content.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// Document-based app @main struct DocumentApp: App { var body: some Scene { DocumentGroup(newDocument: MyDocument()) { file in DocumentView(document: file.$document) } .commands { // Document-specific commands CommandGroup(after: .saveItem) { Button("Export as PDF...") { // Export } .keyboardShortcut("e", modifiers: [.command, .shift]) } } } }
struct DocumentView: View { @Binding var document: MyDocument @FocusedValue(.document) private var focusedDocument
var body: some View {
TextEditor(text: $document.content)
.font(.body.monospaced())
.focusedValue(\.document, $document)
}
}
Reference File Documents (Large Files)
import SwiftUI import UniformTypeIdentifiers
// For large files, use ReferenceFileDocument @Observable class ImageDocument: ReferenceFileDocument { static var readableContentTypes: [UTType] { [.png, .jpeg] } static var writableContentTypes: [UTType] { [.png] }
var image: NSImage?
var annotations: [Annotation] = []
init() {}
required init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
image = NSImage(data: data)
}
func snapshot(contentType: UTType) throws -> Data {
guard let image, let data = image.tiffRepresentation else {
throw CocoaError(.fileWriteUnknown)
}
return data
}
func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: snapshot)
}
}
@main struct ImageEditorApp: App { var body: some Scene { DocumentGroup(newDocument: { ImageDocument() }) { file in ImageEditorView(document: file.document) } } }
Mac Catalyst
Enabling Mac Catalyst
In Xcode: Target → General → Deployment Info → Mac (Mac Catalyst)
Platform-Specific Code
struct ContentView: View { var body: some View { VStack { #if targetEnvironment(macCatalyst) // Mac Catalyst specific UI MacToolbar() #else // iOS specific UI iOSToolbar() #endif
MainContent()
}
}
}
// Check at runtime struct PlatformAwareView: View { var body: some View { Group { if ProcessInfo.processInfo.isMacCatalystApp { MacLayout() } else { iOSLayout() } } } }
Catalyst-Specific Features
#if targetEnvironment(macCatalyst) import AppKit
extension View { func configureMacWindow() -> some View { self.onAppear { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
// Window size
windowScene.sizeRestrictions?.minimumSize = CGSize(width: 800, height: 600)
windowScene.sizeRestrictions?.maximumSize = CGSize(width: 1920, height: 1080)
// Title bar
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
}
}
} #endif
Optimizing for Mac
struct CatalystOptimizedApp: App { var body: some Scene { WindowGroup { ContentView() #if targetEnvironment(macCatalyst) .frame(minWidth: 800, minHeight: 600) .onAppear(perform: setupMacEnvironment) #endif } #if targetEnvironment(macCatalyst) .commands { // Mac-specific menu commands CommandGroup(replacing: .help) { Button("MyApp Help") { // Open help } } } #endif }
#if targetEnvironment(macCatalyst)
private func setupMacEnvironment() {
// Enable hover effects
// Configure pointer interactions
// Set up keyboard shortcuts
}
#endif
}
// Pointer/hover support struct HoverButton: View { @State private var isHovered = false
var body: some View {
Button("Click Me") { }
.buttonStyle(.borderedProminent)
.scaleEffect(isHovered ? 1.05 : 1.0)
.onHover { hovering in
withAnimation(.easeInOut(duration: 0.15)) {
isHovered = hovering
}
}
}
}
macOS-Specific Modifiers
Toolbar Customization
struct MacToolbarView: View { @State private var searchText = ""
var body: some View {
NavigationSplitView {
Sidebar()
} detail: {
DetailView()
}
.toolbar {
// Leading items
ToolbarItem(placement: .navigation) {
Button(action: {}) {
Image(systemName: "sidebar.left")
}
}
// Principal (center)
ToolbarItem(placement: .principal) {
Picker("View", selection: .constant(0)) {
Text("Grid").tag(0)
Text("List").tag(1)
}
.pickerStyle(.segmented)
.frame(width: 150)
}
// Trailing items
ToolbarItemGroup(placement: .primaryAction) {
Button(action: {}) {
Image(systemName: "plus")
}
Button(action: {}) {
Image(systemName: "square.and.arrow.up")
}
}
// Search field (trailing)
ToolbarItem(placement: .automatic) {
TextField("Search", text: $searchText)
.textFieldStyle(.roundedBorder)
.frame(width: 200)
}
}
.toolbarBackground(.visible, for: .windowToolbar)
}
}
Sidebar and Split View
struct ThreeColumnLayout: View { @State private var selectedFolder: Folder? @State private var selectedItem: Item? @State private var columnVisibility: NavigationSplitViewVisibility = .all
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
// Sidebar (first column)
List(folders, selection: $selectedFolder) { folder in
NavigationLink(value: folder) {
Label(folder.name, systemImage: folder.icon)
}
}
.navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 250)
} content: {
// Content (second column)
if let folder = selectedFolder {
List(folder.items, selection: $selectedItem) { item in
NavigationLink(value: item) {
ItemRow(item: item)
}
}
} else {
ContentUnavailableView("Select a Folder", systemImage: "folder")
}
} detail: {
// Detail (third column)
if let item = selectedItem {
ItemDetailView(item: item)
} else {
ContentUnavailableView("Select an Item", systemImage: "doc")
}
}
.navigationSplitViewStyle(.balanced)
}
@State private var folders: [Folder] = []
}
Settings/Preferences Window
@main struct PreferencesApp: App { var body: some Scene { WindowGroup { ContentView() }
#if os(macOS)
Settings {
SettingsView()
}
#endif
}
}
struct SettingsView: View { var body: some View { TabView { GeneralSettings() .tabItem { Label("General", systemImage: "gear") }
AppearanceSettings()
.tabItem {
Label("Appearance", systemImage: "paintbrush")
}
AccountSettings()
.tabItem {
Label("Account", systemImage: "person.crop.circle")
}
AdvancedSettings()
.tabItem {
Label("Advanced", systemImage: "gearshape.2")
}
}
.frame(width: 450, height: 300)
}
}
struct GeneralSettings: View { @AppStorage("launchAtLogin") private var launchAtLogin = false @AppStorage("checkForUpdates") private var checkForUpdates = true
var body: some View {
Form {
Toggle("Launch at Login", isOn: $launchAtLogin)
Toggle("Check for Updates Automatically", isOn: $checkForUpdates)
}
.padding()
}
}
Inspector Panel
struct InspectorView: View { @State private var showInspector = true @State private var selectedItem: Item?
var body: some View {
NavigationStack {
ContentList(selection: $selectedItem)
}
.inspector(isPresented: $showInspector) {
if let item = selectedItem {
ItemInspector(item: item)
} else {
ContentUnavailableView("No Selection", systemImage: "sidebar.right")
}
}
.inspectorColumnWidth(min: 250, ideal: 300, max: 400)
.toolbar {
ToolbarItem {
Button {
showInspector.toggle()
} label: {
Image(systemName: "sidebar.right")
}
}
}
}
}
Keyboard Shortcuts
Custom Keyboard Shortcuts
struct KeyboardShortcutDemo: View { @State private var text = ""
var body: some View {
TextEditor(text: $text)
.toolbar {
// Toolbar button with shortcut
Button("Bold") {
makeBold()
}
.keyboardShortcut("b", modifiers: .command)
Button("Italic") {
makeItalic()
}
.keyboardShortcut("i", modifiers: .command)
}
// Background keyboard shortcuts
.background {
Group {
Button("") { duplicateLine() }
.keyboardShortcut("d", modifiers: [.command, .shift])
Button("") { deleteLine() }
.keyboardShortcut(.delete, modifiers: [.command, .shift])
}
.frame(width: 0, height: 0)
.opacity(0)
}
}
private func makeBold() {}
private func makeItalic() {}
private func duplicateLine() {}
private func deleteLine() {}
}
Focus-Based Commands
struct FocusedDocument: FocusedValueKey { typealias Value = Binding<MyDocument> }
extension FocusedValues { var document: Binding<MyDocument>? { get { self[FocusedDocument.self] } set { self[FocusedDocument.self] = newValue } } }
struct DocumentCommands: Commands { @FocusedValue(.document) var document
var body: some Commands {
CommandGroup(after: .textEditing) {
Button("Word Count") {
if let doc = document?.wrappedValue {
let count = doc.content.split(separator: " ").count
print("Words: \(count)")
}
}
.keyboardShortcut("w", modifiers: [.command, .shift])
.disabled(document == nil)
}
}
}
Best Practices
DO
// ✓ Use native macOS controls NavigationSplitView { ... }
// ✓ Support keyboard navigation .focusable() .onKeyPress(.tab) { ... }
// ✓ Respect system appearance @Environment(.colorScheme) var colorScheme
// ✓ Use Settings scene for preferences Settings { SettingsView() }
// ✓ Support window restoration .handlesExternalEvents(matching: ["main"])
DON'T
// ✗ Don't use iOS-only patterns .navigationBarHidden(true) // Use .toolbar instead
// ✗ Don't ignore keyboard shortcuts // Always add for common actions
// ✗ Don't hardcode window sizes .frame(width: 800, height: 600) // Use defaultSize instead
// ✗ Don't forget right-click menus // Add contextMenu to interactive elements
Official Resources
-
Mac App Documentation
-
Human Interface Guidelines - macOS
-
Mac Catalyst Documentation
-
WWDC23: Build programmatic UI with Xcode Previews