← back to blog
11 min read

Building macOS Menu Bar Apps: A Complete Guide

Everything you need to know to build a polished macOS menu bar application with SwiftUI, from setup to distribution.

macOSSwiftUITutorial

Menu bar apps are perfect for utilities that users need quick access to. After building Tidey and other menu bar apps, here's my complete guide.

Project Setup

Create a new macOS app in Xcode, then modify the App struct:

import SwiftUI

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

var body: some Scene { Settings { EmptyView() } } }

The key is using an AppDelegate to manage the status item:

class AppDelegate: NSObject, NSApplicationDelegate {
    var statusItem: NSStatusItem?
    var popover: NSPopover?

func applicationDidFinishLaunching(_ notification: Notification) { setupMenuBar() }

func setupMenuBar() { statusItem = NSStatusBar.system.statusItem( withLength: NSStatusItem.squareLength )

if let button = statusItem?.button { button.image = NSImage( systemSymbolName: "star.fill", accessibilityDescription: "App Menu" ) button.action = #selector(togglePopover) }

popover = NSPopover() popover?.contentSize = NSSize(width: 300, height: 400) popover?.behavior = .transient popover?.contentViewController = NSHostingController( rootView: ContentView() ) }

@objc func togglePopover() { guard let button = statusItem?.button else { return }

if let popover = popover { if popover.isShown { popover.performClose(nil) } else { popover.show( relativeTo: button.bounds, of: button, preferredEdge: .minY )

// Activate app to receive keyboard events NSApp.activate(ignoringOtherApps: true) } } } }

The Popover Content

Your SwiftUI view works normally:

struct ContentView: View {
    @State private var items: [Item] = []

var body: some View { VStack(spacing: 0) { // Header HStack { Text("My App") .font(.headline) Spacer() Button("Quit") { NSApp.terminate(nil) } .buttonStyle(.plain) } .padding()

Divider()

// Content List(items) { item in ItemRow(item: item) }

Divider()

// Footer HStack { Button("Settings") { openSettings() } Spacer() } .padding() } } }

Hiding the Dock Icon

Menu bar apps typically don't show in the Dock. Add to Info.plist:

<key>LSUIElement</key>
<true/>

Launch at Login

Use ServiceManagement:

import ServiceManagement

func setLaunchAtLogin(_ enabled: Bool) { try? SMAppService.mainApp.register() // or .unregister() to disable }

Global Keyboard Shortcuts

Register a global hotkey:

import Carbon

func registerHotkey() { var hotKeyRef: EventHotKeyRef? var hotKeyID = EventHotKeyID() hotKeyID.signature = OSType("MYAP".fourCharCodeValue) hotKeyID.id = 1

// Command + Shift + T RegisterEventHotKey( UInt32(kVK_ANSI_T), UInt32(cmdKey | shiftKey), hotKeyID, GetEventDispatcherTarget(), 0, &hotKeyRef ) }

Handling Permissions

For file access (like cleaning the desktop):

func requestDesktopAccess() {
    let desktopURL = FileManager.default.urls(
        for: .desktopDirectory,
        in: .userDomainMask
    ).first!

let openPanel = NSOpenPanel() openPanel.directoryURL = desktopURL openPanel.canChooseDirectories = true openPanel.canChooseFiles = false openPanel.prompt = "Grant Access" openPanel.message = "Please grant access to your Desktop folder"

openPanel.begin { response in if response == .OK { // Save bookmark for future access saveSecurityScopedBookmark(openPanel.url!) } } }

Distribution

For the Mac App Store:

  • Enable App Sandbox in capabilities
  • Request only necessary entitlements
  • Sign with your distribution certificate
  • For direct distribution:

  • Notarize with xcrun notarytool
  • Staple the ticket: xcrun stapler staple MyApp.app
  • Create a DMG for distribution
  • Common Pitfalls

  • Popover closes on click outside - This is .transient behavior. Use .applicationDefined if you need it to stay open.
  • Keyboard shortcuts don't work - Call NSApp.activate(ignoringOtherApps: true) when showing popover.
  • App doesn't quit properly - Implement applicationShouldTerminateAfterLastWindowClosed returning false.
  • Menu bar apps are satisfying to build - small, focused, and immediately useful.