- Skill ID
- fayazara/macos-app-skills/macos-patterns
- Publisher
- fayazara
- Repository
- macos-app-skills
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
fayazara/macos-app-skills/macos-patternsInstalls these files- SKILL.md
What this skill tells the agent
Native macOS Patterns for Web Developers
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.
Menu Bar Apps
There are two approaches. Use MenuBarExtra for simple menus, NSStatusItem for full control.
SwiftUI MenuBarExtra (simple)
@main
struct MyApp: App {
var body: some Scene {
MenuBarExtra("MyApp", image: "MenuBarIcon") {
MenuBarView()
}
}
}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:
MenuBarExtra("MyApp", image: "MenuBarIcon") {
PopoverContentView()
}
.menuBarExtraStyle(.window)AppKit NSStatusItem (full control)
For custom menus, dynamic icons, or complex interactions:
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem.button {
let icon = NSImage(named: "MenuBarIcon")!
icon.size = NSSize(width: 18, height: 18)
icon.isTemplate = true // CRITICAL: adapts to dark/light mode automatically
button.image = icon
}
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ","))
statusItem.menu = menuisTemplate = true is essential. Without it, your icon will be invisible in light mode or look wrong in dark mode.
NSPopover for Rich Menu Bar Content
For a popover attached to the menu bar icon (like Bartender, iStatMenus):
let popover = NSPopover()
popover.contentSize = NSSize(width: 300, height: 400)
popover.behavior = .transient // auto-closes when clicking outside
popover.contentViewController = NSHostingController(rootView: MyPopoverView())
// Show from the status item button:
if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
}Activation Policy -- Dock Icon Toggling
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.
// At launch -- hide from Dock:
NSApp.setActivationPolicy(.accessory)
// When opening a window -- show in Dock:
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
// When closing the last window -- hide again:
NSApp.setActivationPolicy(.accessory)Use reference counting if multiple windows can be open simultaneously:
@MainActor
enum AppActivationPolicy {
private static var count = 0
static func enter() {
count += 1
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
static func leave() {
count = max(0, count - 1)
guard count == 0 else { return }
Task { @MainActor in NSApp.setActivationPolicy(.accessory) }
}
}NSPanel vs NSWindow
NSPanel is a subclass of NSWindow for auxiliary content that should not steal focus. Use it for:
- Floating overlays (preview cards, recording indicators)
- Palettes and tool windows
- Inspectors
- Anything that should stay visible while the user works in another app
Key configuration:
let panel = NSPanel(
contentRect: .zero,
styleMask: [.borderless, .nonactivatingPanel], // Does NOT steal focus
backing: .buffered,
defer: false
)
panel.isOpaque = false
panel.backgroundColor = .clear
panel.hasShadow = false
panel.hidesOnDeactivate = false // Stay visible when app loses focus
panel.isFloatingPanel = true // Float above normal windows
panel.ignoresMouseEvents = true // Clicks pass through to windows below
panel.collectionBehavior = [
.canJoinAllSpaces, // Visible on all virtual desktops
.fullScreenAuxiliary, // Visible over fullscreen apps
]To host SwiftUI content in the panel:
let hostingView = NSHostingView(rootView: MySwiftUIView())
panel.contentView = hostingViewOverride focus behavior when needed:
class MyPanel: NSPanel {
override var canBecomeKey: Bool { true } // Can receive keyboard input
override var canBecomeMain: Bool { false } // Never becomes the "main" window
}Screen Capture Exclusion
If your app shows floating UI that shouldn't appear in screenshots/recordings:
panel.sharingType = .none // Invisible to screen capture APIsWindow Levels
macOS has a multi-tier window level system that controls where windows appear relative to the entire OS (not just your app):
normal (0) -- standard app windows
floating (3) -- tool palettes, floating panels
modalPanel (8) -- modal dialogs
mainMenu (24) -- menu bar
screenSaver (1000) -- above everything except...
CGShieldingWindowLevel() -- above absolutely everything (kiosk mode)Set via:
window.level = .floating
// or for extreme cases:
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))Collection Behaviors
Control how windows interact with Spaces, fullscreen, and Cmd-Tab:
window.collectionBehavior = [
.canJoinAllSpaces, // Visible on all virtual desktops
.fullScreenAuxiliary, // Visible over fullscreen apps
.stationary, // Doesn't move with Space transitions
.ignoresCycle, // Hidden from Cmd-` window cycling
.transient, // Removed when app is hidden
]Screen Geometry
macOS uses bottom-left origin coordinates. The Y axis is flipped compared to the web.
let screen = NSScreen.main!
// Full screen rectangle (includes menu bar and Dock area):
screen.frame // e.g., (0, 0, 1728, 1117)
// Usable area (excludes menu bar and Dock):
screen.visibleFrame // e.g., (0, 0, 1728, 1055)
// Notch detection (MacBook with notch has top safe area):
screen.safeAreaInsets.top > 0 // true on notch MacsUse frame when positioning over the menu bar (notch overlays). Use visibleFrame for normal window placement.
Quartz vs AppKit Y-Axis
Core Graphics / Quartz uses top-left origin. AppKit uses bottom-left origin. When converting between the two:
let desktopFrame = NSScreen.screens.reduce(CGRect.null) { $0.union($1.frame) }
let appKitY = desktopFrame.maxY - quartzY - height // Quartz → AppKit
let quartzY = desktopFrame.maxY - appKitY - height // AppKit → QuartzMulti-Monitor
Never assume a single screen. Always handle the case where NSScreen.main is not the only display:
// Find the screen containing the mouse pointer:
let mouseLocation = NSEvent.mouseLocation
let screen = NSScreen.screens.first { $0.frame.contains(mouseLocation) }
// Find the screen containing a specific window:
let screen = window.screenKeyboard Shortcuts
There are 3 tiers, each for different use cases.
Tier 1: SwiftUI keyboard shortcuts (in-app, when focused)
Button("Settings") { openSettings() }
.keyboardShortcut(",", modifiers: [.command]) // Cmd+,Tier 2: NSEvent monitors (app-wide or global)
// Local: fires only when YOUR app is active
let monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
if event.keyCode == 53 { /* Escape */ return nil /* consume */ }
return event // pass through
}
// Global: fires even when ANOTHER app is active
let monitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
// Cannot consume the event -- read-only
}
// CRITICAL: Always remove monitors when done
NSEvent.removeMonitor(monitor)More from fayazara
Use this skill in a project
Open any RiverX project, search the Skills panel for fayazara/macos-app-skills/macos-patterns, and install it in one click.
