All skills

Comprehensive macOS development guidance including Swift 6+, SwiftUI, SwiftData, architecture patterns, AppKit bridging, and macOS 26 Tahoe APIs. Use for macOS code review, best practices, UI review, or platform-specific features.

  • 40 files
  • 300.9 KB
  • Updated 3 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/macos

This session only. Nothing lands on disk.

macos-capabilitiesmenubar.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Menu Bar Apps

Building menu bar applications with MenuBarExtra (SwiftUI) and NSStatusItem (AppKit). Covers background-only apps, popover menus, and hybrid architectures.

MenuBarExtra (SwiftUI, macOS 13+)

The modern, declarative way to build menu bar apps.

Simple Menu

@main
struct MyMenuBarApp: App {
    var body: some Scene {
        MenuBarExtra("MyApp", systemImage: "star.fill") {
            Button("Open Dashboard") {
                // Action
            }
            Button("Check for Updates") {
                // Action
            }
            Divider()
            Button("Quit") {
                NSApplication.shared.terminate(nil)
            }
            .keyboardShortcut("q")
        }
    }
}

Window-Style Popover

For richer UIs, use .menuBarExtraStyle(.window):

@main
struct StatusApp: App {
    @State private var appState = AppState()

    var body: some Scene {
        MenuBarExtra("Status", systemImage: appState.statusIcon) {
            StatusPopoverView()
                .environment(appState)
                .frame(width: 320, height: 400)
        }
        .menuBarExtraStyle(.window)

        // Optional: main window
        Window("Settings", id: "settings") {
            SettingsView()
                .environment(appState)
        }
    }
}

struct StatusPopoverView: View {
    @Environment(AppState.self) private var appState
    @Environment(\.dismiss) private var dismiss
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // Header
            HStack {
                Text("System Status")
                    .font(.headline)
                Spacer()
                Button("Settings", systemImage: "gear") {
                    openWindow(id: "settings")
                    NSApp.activate(ignoringOtherApps: true)
                    dismiss()
                }
                .buttonStyle(.borderless)
            }

            Divider()

            // Content
            ForEach(appState.statusItems) { item in
                StatusRow(item: item)
            }

            Divider()

            // Footer
            HStack {
                Text("Last updated: \(appState.lastUpdate.formatted())")
                    .font(.caption)
                    .foregroundStyle(.secondary)
                Spacer()
                Button("Refresh") {
                    Task { await appState.refresh() }
                }
                .controlSize(.small)
            }
        }
        .padding()
    }
}

Dynamic Icon

Update the menu bar icon based on state:

@Observable class AppState {
    var isConnected = false
    var unreadCount = 0

    var statusIcon: String {
        if !isConnected { return "wifi.slash" }
        if unreadCount > 0 { return "bell.badge.fill" }
        return "bell.fill"
    }
}

// The icon updates automatically when properties change
MenuBarExtra("Status", systemImage: appState.statusIcon) { ... }

Dismissing the Menu After Actions

The menu should dismiss after the user clicks an action item:

struct MenuView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open Main Window") {
            openWindow(id: "main")
            NSApp.activate(ignoringOtherApps: true)
            dismiss()  // Dismiss after action
        }
    }
}

NSStatusItem (AppKit)

For more control over the menu bar item, use AppKit's NSStatusItem directly.

Basic Setup

class StatusBarController {
    private var statusItem: NSStatusItem?

    func setup() {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

        if let button = statusItem?.button {
            button.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "MyApp")
            button.action = #selector(handleClick(_:))
            button.target = self
            button.sendAction(on: [.leftMouseUp, .rightMouseUp])
        }
    }

    @objc func handleClick(_ sender: NSStatusBarButton) {
        let event = NSApp.currentEvent!
        if event.type == .rightMouseUp {
            showContextMenu()
        } else {
            togglePopover()
        }
    }

    func showContextMenu() {
        let menu = NSMenu()
        menu.addItem(NSMenuItem(title: "Settings...", action: #selector(openSettings), keyEquivalent: ","))
        menu.addItem(.separator())
        menu.addItem(NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q"))
        statusItem?.menu = menu
        statusItem?.button?.performClick(nil)
        statusItem?.menu = nil  // Reset so left-click works again
    }
}

Popover with SwiftUI Content

class StatusBarController {
    private var statusItem: NSStatusItem?
    private var popover: NSPopover?

    func setup() {
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem?.button?.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "MyApp")
        statusItem?.button?.action = #selector(togglePopover)
        statusItem?.button?.target = self

        popover = NSPopover()
        popover?.contentSize = NSSize(width: 320, height: 400)
        popover?.behavior = .transient  // Auto-dismiss when clicking outside
        popover?.contentViewController = NSHostingController(
            rootView: PopoverContentView()
        )
    }

    @objc func togglePopover() {
        guard let popover, let button = statusItem?.button else { return }
        if popover.isShown {
            popover.performClose(nil)
        } else {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
            NSApp.activate(ignoringOtherApps: true)
        }
    }
}

Background-Only App Architecture

A menu bar app with no Dock icon and no main window:

Info.plist Configuration

<!-- Hide from Dock -->
<key>LSUIElement</key>
<true/>

SwiftUI Approach

@main
struct BackgroundApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        MenuBarExtra("MyUtil", systemImage: "gearshape") {
            MenuView()
        }
        .menuBarExtraStyle(.window)

        // Settings window (opens on demand)
        Settings {
            SettingsView()
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        // No main window to show
    }

    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
        // Clicking Dock icon (if visible) doesn't open a window
        false
    }
}

Activation Policy Switching

Toggle between Dock and menu-bar-only mode:

@Observable class AppMode {
    var showInDock: Bool = false {
        didSet {
            NSApp.setActivationPolicy(showInDock ? .regular : .accessory)
        }
    }
}

Common Patterns

Status Item with Badge Count

func updateBadge(count: Int) {
    guard let button = statusItem?.button else { return }
    if count > 0 {
        button.title = " \(count)"
        button.imagePosition = .imageLeading
    } else {
        button.title = ""
        button.imagePosition = .imageOnly
    }
}

Opening Windows from Menu Bar

// Problem: openWindow() opens the window but doesn't bring it to front
// Solution: activate the app after opening

Button("Show Dashboard") {
    openWindow(id: "dashboard")
    NSApp.activate(ignoringOtherApps: true)
    dismiss()  // Close the menu
}

Global Keyboard Shortcut

Register a hotkey to toggle the menu bar popover:

import Carbon

class HotkeyManager {
    private var hotkeyRef: EventHotKeyRef?

    func register(keyCode: UInt32, modifiers: UInt32, handler: @escaping () -> Void) {
        var hotKeyID = EventHotKeyID()
        hotKeyID.signature = OSType(0x4D794170) // "MyAp"
        hotKeyID.id = 1

        var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
                                       eventKind: UInt32(kEventHotKeyPressed))

        InstallEventHandler(GetApplicationEventTarget(), { _, event, _ -> OSStatus in
            // Handler called on hotkey press
            return noErr
        }, 1, &eventType, nil, nil)

        RegisterEventHotKey(keyCode, modifiers, hotKeyID,
                           GetApplicationEventTarget(), 0, &hotkeyRef)
    }
}

Best Practices

  1. Use MenuBarExtra for new apps - Simpler, declarative, less code
  2. Always provide a Quit option - Users expect it in menu bar apps
  3. Dismiss the menu after actions - Standard macOS behavior
  4. Activate the app when opening windows - Use NSApp.activate(ignoringOtherApps: true)
  5. Keep menus lightweight - Don't do heavy computation in the menu view
  6. Support keyboard shortcuts - Add .keyboardShortcut() to menu items
  7. Use SF Symbols for the status item - Automatic template rendering for dark/light menu bar

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 32566aa. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files Network
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 4 allowed tools
ReadGlobGrepWebFetch

README badge

README badge for rshankras/claude-code-apple-skills/macos