Build Mac AppDocsMenu

Modules

Module: a menubar (agent) app

The template ships a window app: a Dock icon, one window, quits when the window closes. Two of the three source projects were agent apps instead — no Dock icon, living in the menu bar or at the edge of the screen — and every rule below is something one of them learned the expensive way. Switch when your product is something that stays out of the way all day.

Everything else in the template — the licence stack, the updater, the main menu, Settings, the release pipeline — carries over unchanged.


1. Become an agent, twice

  • mac/App/Resources/Info.plist: add LSUIElement = true. It governs the process from the instant of launch, so there is no Dock icon flash.
  • AppDelegate.applicationDidFinishLaunching: call NSApp.setActivationPolicy(.accessory) as well, and return false from applicationShouldTerminateAfterLastWindowClosed — closing Settings must not quit the app.

Both, not one: the plist key alone flashes nothing at launch but leaves the policy to drift later; the runtime call alone flashes a Dock icon at launch.

2. The activation dance, for every window with a text field

An .accessory app cannot make a window key, so a text field in it cannot be typed into. When a real window opens (the licence gate, Settings):

NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()

and when the last such window closes, go back to .accessory. Becoming .regular earns a Dock icon and a slot in the menu bar; it does not populate a menu — the main menu from MainMenu.swift still has to be installed at launch, or ⌘V is dead in the licence field. Three comments in a source project claimed .regular was what "makes ⌘V work"; every part of that looked done.

Also activate before anything that puts a window on screen by itself — UpdateController.checkForUpdates() already does, because Sparkle's window otherwise opens behind whatever the user is looking at.

3. Quit needs a home

With no menu bar of your own, ⌘Q only reaches the app while one of its windows is active. Keep Quit in Settings → General (the template already has it) and in the status item's menu. On the licence gate, Quit must be reachable before a key is entered — somebody who cannot find their key must not be left with Force Quit, thirty seconds after paying.

4. The status item, or the panel

  • MenuBarExtra(.window) in SwiftUI is the quick route for a popover-style panel from the menu bar. It needs a SwiftUI App, which conflicts with the template's AppKit shell (main.swift explains why the delegate must exist before the run loop); an NSStatusItem owned by AppDelegate keeps that.
  • A floating NSPanel (a widget at the screen edge) has rules nothing but the window server can confirm:
    • assign level after isFloatingPanel: the latter's setter resets the level, silently, and the panel loses stacking arguments to other apps' HUDs weeks later on somebody else's Mac;
    • set NSHostingView.sizingOptions = [], or SwiftUI resizes the window to its intrinsic size and becomes a second authority on its frame;
    • .nonactivatingPanel with canBecomeKey overridden to true when it has a text field;
    • dock it to NSScreen.screens.first, recomputed on every change, so it cannot be stranded on an unplugged display.
  • Verify those properties by reading the window back (CGWindowListCopyWindowInfo), and never while the screen is locked: a locked display reports window bounds at roughly 0.9×, so every check fails naming numbers that match nothing in the source.

5. The licence in an app that never quits

LicenseController.revalidateIfDue() runs once per launch in the window app, which is a check every few days in practice. An agent app may run for weeks, so check on a timer as well — hourly is plenty; it only talks to the server once revalidation is due. Keep the product's promise about when the answer lands: never yank the product out from under somebody mid-task. Apply a lapse at the next natural boundary — the next time a window opens, or at next launch.

6. Screen sharing and presence

If the app ever draws over other apps (a celebration, an overlay), set sharingType = .none on that window — it is the only reliable way to keep it out of a screen share, because macOS exposes no API for "my screen is being shared". The window then cannot be screenshotted by anyone, the user included; that is the flag working. Do not read ~/Library/DoNotDisturb/ to detect Focus — it needs Full Disk Access, which is a lot to ask for a sound effect.

7. The site

  • The FAQ and privacy page say the app "runs only when you open it" in spirit; an agent app runs at login. Say so, and say it does nothing over the network besides the licence check and updates.
  • Screens rendered for the landing page (docs/modules/marketing-shots.md) must show the status item or panel, not a window the product does not have.

Verify

  • mac/scripts/verify-menu.sh still passes — the main menu is installed even though no menu bar is drawn while .accessory.
  • Paste a licence key with ⌘V into the gate, with the app in the background beforehand.
  • Close Settings: the app keeps running. Quit from Settings: it quits.

This page is docs/modules/menubar.md in the repository, copied 2026-09-25.