macos-development
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
More from bluewaves-creations/bluewaves-skills
photographer-testino
Generate images in Mario Testino's glamorous vibrant style. Use when users ask for Testino style, high fashion glamour, bold saturated colors, warm luxurious photography, dynamic sensual energy.
35photographer-lindbergh
Generate images in Peter Lindbergh's iconic black and white style. Use when users ask for Lindbergh style, raw authentic beauty, emotional B&W portraits, supermodel aesthetic, or unretouched natural photography.
30photographer-lachapelle
Generate images in David LaChapelle's surreal pop art style. Use when users ask for LaChapelle style, pop surrealism, hyper-saturated colors, theatrical staging, baroque maximalism, kitsch aesthetic.
24epub-creator
Create production-quality EPUB 3 ebooks from markdown and images with automated QA, formatting fixes, and validation. Use when creating ebooks, converting markdown to EPUB, or compiling chapters into a publishable book. Handles markdown quirks, generates TOC, adds covers, and validates output automatically.
22photographer-vonunwerth
Generate images in Ellen von Unwerth's playful vintage style. Use when users ask for von Unwerth style, playful sensuality, vintage film noir, whimsical feminine photography, retro glamour, narrative storytelling.
19photographer-ritts
Generate images in Herb Ritts' sculptural black and white style. Use when users ask for Ritts style, classical Greek aesthetic, sculptural body photography, California golden hour, minimalist athletic portraits.
18