$npx -y skills add fayazara/macos-app-skills --skill macos-patternsEssential native macOS development patterns that web developers don't know about. Use this skill whenever the user is building a macOS app and needs guidance on native patterns, or when they ask about menu bar apps, floating panels, window levels, keyboard shortcuts, file pickers
| 1 | # Native macOS Patterns for Web Developers |
| 2 | |
| 3 | This is a reference guide for the macOS-specific patterns that have no web equivalent. When building a native macOS app, these are the things that trip up everyone coming from web development. The AI should consult this before generating macOS code to avoid confidently producing patterns that don't work. |
| 4 | |
| 5 | ## Menu Bar Apps |
| 6 | |
| 7 | There are two approaches. Use `MenuBarExtra` for simple menus, `NSStatusItem` for full control. |
| 8 | |
| 9 | ### SwiftUI MenuBarExtra (simple) |
| 10 | |
| 11 | ```swift |
| 12 | @main |
| 13 | struct MyApp: App { |
| 14 | var body: some Scene { |
| 15 | MenuBarExtra("MyApp", image: "MenuBarIcon") { |
| 16 | MenuBarView() |
| 17 | } |
| 18 | } |
| 19 | } |
| 20 | ``` |
| 21 | |
| 22 | This creates a menu-bar-only app. The menu content is a standard SwiftUI view. For a popover-style menu bar app (richer UI than a plain menu), use the `.window` style: |
| 23 | |
| 24 | ```swift |
| 25 | MenuBarExtra("MyApp", image: "MenuBarIcon") { |
| 26 | PopoverContentView() |
| 27 | } |
| 28 | .menuBarExtraStyle(.window) |
| 29 | ``` |
| 30 | |
| 31 | ### AppKit NSStatusItem (full control) |
| 32 | |
| 33 | For custom menus, dynamic icons, or complex interactions: |
| 34 | |
| 35 | ```swift |
| 36 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) |
| 37 | if let button = statusItem.button { |
| 38 | let icon = NSImage(named: "MenuBarIcon")! |
| 39 | icon.size = NSSize(width: 18, height: 18) |
| 40 | icon.isTemplate = true // CRITICAL: adapts to dark/light mode automatically |
| 41 | button.image = icon |
| 42 | } |
| 43 | |
| 44 | let menu = NSMenu() |
| 45 | menu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ",")) |
| 46 | statusItem.menu = menu |
| 47 | ``` |
| 48 | |
| 49 | `isTemplate = true` is essential. Without it, your icon will be invisible in light mode or look wrong in dark mode. |
| 50 | |
| 51 | ### NSPopover for Rich Menu Bar Content |
| 52 | |
| 53 | For a popover attached to the menu bar icon (like Bartender, iStatMenus): |
| 54 | |
| 55 | ```swift |
| 56 | let popover = NSPopover() |
| 57 | popover.contentSize = NSSize(width: 300, height: 400) |
| 58 | popover.behavior = .transient // auto-closes when clicking outside |
| 59 | popover.contentViewController = NSHostingController(rootView: MyPopoverView()) |
| 60 | |
| 61 | // Show from the status item button: |
| 62 | if let button = statusItem.button { |
| 63 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY) |
| 64 | } |
| 65 | ``` |
| 66 | |
| 67 | ## Activation Policy -- Dock Icon Toggling |
| 68 | |
| 69 | A macOS app can dynamically show/hide its Dock icon and Cmd-Tab presence at runtime. Menu-bar-only apps start as `.accessory` (invisible in Dock) and temporarily become `.regular` when they open windows like Settings. |
| 70 | |
| 71 | ```swift |
| 72 | // At launch -- hide from Dock: |
| 73 | NSApp.setActivationPolicy(.accessory) |
| 74 | |
| 75 | // When opening a window -- show in Dock: |
| 76 | NSApp.setActivationPolicy(.regular) |
| 77 | NSApp.activate(ignoringOtherApps: true) |
| 78 | |
| 79 | // When closing the last window -- hide again: |
| 80 | NSApp.setActivationPolicy(.accessory) |
| 81 | ``` |
| 82 | |
| 83 | Use reference counting if multiple windows can be open simultaneously: |
| 84 | |
| 85 | ```swift |
| 86 | @MainActor |
| 87 | enum AppActivationPolicy { |
| 88 | private static var count = 0 |
| 89 | |
| 90 | static func enter() { |
| 91 | count += 1 |
| 92 | NSApp.setActivationPolicy(.regular) |
| 93 | NSApp.activate(ignoringOtherApps: true) |
| 94 | } |
| 95 | |
| 96 | static func leave() { |
| 97 | count = max(0, count - 1) |
| 98 | guard count == 0 else { return } |
| 99 | Task { @MainActor in NSApp.setActivationPolicy(.accessory) } |
| 100 | } |
| 101 | } |
| 102 | ``` |
| 103 | |
| 104 | ## NSPanel vs NSWindow |
| 105 | |
| 106 | `NSPanel` is a subclass of `NSWindow` for auxiliary content that should not steal focus. Use it for: |
| 107 | |
| 108 | - Floating overlays (preview cards, recording indicators) |
| 109 | - Palettes and tool windows |
| 110 | - Inspectors |
| 111 | - Anything that should stay visible while the user works in another app |
| 112 | |
| 113 | Key configuration: |
| 114 | |
| 115 | ```swift |
| 116 | let panel = NSPanel( |
| 117 | contentRect: .zero, |
| 118 | styleMask: [.borderless, .nonactivatingPanel], // Does NOT steal focus |
| 119 | backing: .buffered, |
| 120 | defer: false |
| 121 | ) |
| 122 | panel.isOpaque = false |
| 123 | panel.backgroundColor = .clear |
| 124 | panel.hasShadow = false |
| 125 | panel.hidesOnDeactivate = false // Stay visible when app loses focus |
| 126 | panel.isFloatingPanel = true // Float above normal windows |
| 127 | panel.ignoresMouseEvents = true // Clicks pass through to windows below |
| 128 | panel.collectionBehavior = [ |
| 129 | .canJoinAllSpaces, // Visible on all virtual desktops |
| 130 | .fullScreenAuxiliary, // Visible over fullscreen apps |
| 131 | ] |
| 132 | ``` |
| 133 | |
| 134 | To host |