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.
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:
For direct distribution:
xcrun notarytoolxcrun stapler staple MyApp.appCommon Pitfalls
.transient behavior. Use .applicationDefined if you need it to stay open.NSApp.activate(ignoringOtherApps: true) when showing popover.applicationShouldTerminateAfterLastWindowClosed returning false.Menu bar apps are satisfying to build - small, focused, and immediately useful.