Menu Bar App Windows That Open Behind Everything

4 min readApple, Xcode & Tools

Jake Marsh
In this post
  1. Why The Window Ends Up Behind
  2. Activating The App
  3. activate(ignoringOtherApps:) And macOS 14
  4. makeKeyAndOrderFront Vs orderFrontRegardless
  5. SwiftUI’s Settings Scene
  6. Borrowing A Dock Icon While A Window Is Open

Most of my Mac apps live in the menu bar. Shake It On, Multimeter and Refrain all set LSUIElement to YES in their Info.plist, so there’s no Dock icon and no app menu, only a status item. Each one still has a Settings window and an About window, and the first time you wire one up, it’s easy to end up with a window that opens behind whatever app you were using.

Here’s why that happens and the handful of calls that fix it.

Why The Window Ends Up Behind#

LSUIElement gives your app the accessory activation policy. AppKit’s header describes it like this:

The application does not appear in the Dock and does not have a menu bar, but it may be activated programmatically or by clicking on one of its windows.

Clicking your status item’s menu doesn’t activate the app. The app you were in stays active, and its windows stay in front. So when the Settings menu item’s action runs window.makeKeyAndOrderFront(nil), the window comes up, but the active app’s windows stay above it.

I checked this with a tiny LSUIElement test app on macOS 27. After makeKeyAndOrderFront(nil), the window was visible, isKeyWindow was false, NSApp.isActive was false and NSApp.keyWindow was nil. Apple’s documentation for activate(ignoringOtherApps:) covers the missing piece: makeKeyAndOrderFront makes the window key “when the app is active.” Until your app activates, nothing you type goes to that window.

Activating The App#

The fix is to activate the app as well as ordering the window. Multimeter does both in one helper that every window goes through:

MultimeterApp.swiftSwift
/// LSUIElement apps aren't active when a status menu item fires, so the
/// window has to be raised and the app activated or it opens behind
/// whatever you were using.
private func show(_ window: NSWindow) {
  NSApp.activate(ignoringOtherApps: true)
  window.makeKeyAndOrderFront(nil)
}

Multimeter does the same before an alert. It calls NSApp.activate(ignoringOtherApps: true) before runModal() on the NSAlert it shows after importing a meter.

Activation also isn’t instant. The header for NSApp.activate() says “You shouldn’t assume the app will be active immediately after sending this message” and that the framework doesn’t guarantee it’ll activate at all. Shake It On and Refrain cover that gap by lifting the window above normal windows for a moment:

WindowManager.swiftSwift
/// Menu bar apps don't own the foreground, so lift the window above
/// everything briefly or it opens behind the frontmost app.
private func present(_ window: NSWindow) {
    NSApp.activate(ignoringOtherApps: true)
    window.level = .floating
    window.makeKeyAndOrderFront(nil)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
        window.level = .normal
    }
}

A .floating window sits above every app’s .normal windows, so it’s visible right away even if activation lands a beat later. After 0.3 seconds it drops back to .normal so it behaves like any other window once you click away.

activate(ignoringOtherApps:) And macOS 14#

macOS 14 added NSApp.activate() with no arguments, along with yieldActivation(to:) and yieldActivation(toApplicationWithBundleIdentifier:). This is what Apple calls cooperative activation: the app that’s active hands activation to another app, and that app then asks for it with activate(). In the SDK the old call is marked API_TO_BE_DEPRECATED with the message “This method will be deprecated in a future release. Use NSApp.activate instead.”

API_TO_BE_DEPRECATED doesn’t produce a warning today. I type-checked NSApp.activate(ignoringOtherApps: true) with deployment targets of macOS 13, 14 and 26, and none of them warned. The NSRunningApplication flag is a different story. activate(options: .activateIgnoringOtherApps) warns on a macOS 14 or later target, and its header says “ignoringOtherApps is deprecated in macOS 14 and will have no effect.”

NSApp.activate() is only available on macOS 14, so it fails to compile in an app that targets 13 without an availability check. Shake It On and Refrain target macOS 14 and could call activate() directly. Multimeter still supports macOS 13, so moving it over takes a check:

Swift
extension NSApplication {
    func activateForWindow() {
        if #available(macOS 14, *) {
            activate()
        } else {
            activate(ignoringOtherApps: true)
        }
    }
}

makeKeyAndOrderFront Vs orderFrontRegardless#

orderFrontRegardless() looks like it was made for this. Apple’s documentation says it moves the window “to the front of its level, even if its application isn’t active, without changing either the key window or the main window.” That last part is the catch for a Settings window. In my test app, after orderFrontRegardless() the window was visible, it wasn’t key and the app wasn’t active, so typing still went to the other app.

Use orderFrontRegardless() for a panel you only need to see, such as a HUD or a status overlay. For a window with text fields or keyboard shortcuts, activate the app and use makeKeyAndOrderFront(nil).

SwiftUI’s Settings Scene#

SwiftUI has two ways to open the Settings scene from code, and both need macOS 14: SettingsLink, a view you put in a menu or window, and the openSettings environment action. The older trick of sending showSettingsWindow: up the responder chain doesn’t open it. In my LSUIElement test app on macOS 27, NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) returned true, and no window appeared.

openSettings does open the window, but it doesn’t activate the app. In the test, calling it from a SwiftUI view in an LSUIElement app opened the Settings window with isKeyWindow false and NSApp.isActive false. So you still call NSApp.activate() yourself, before or right after.

The other problem is reaching openSettings at all. It’s an environment value, so only a SwiftUI view can read it. Multimeter builds its menu with NSStatusItem and NSMenu, which aren’t views. Shake It On and Refrain use MenuBarExtra, where SettingsLink would fit, but all three host their SwiftUI settings view in an NSWindow they own and show it with an activate-and-raise helper like the ones above:

Swift
let window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 480, height: 640),
    styleMask: [.titled, .closable, .miniaturizable],
    backing: .buffered,
    defer: false
)
window.title = "Settings"
window.contentView = NSHostingView(rootView: SettingsView())
window.isReleasedWhenClosed = false
window.center()

Multimeter’s App body still declares Settings { EmptyView() }, because a SwiftUI App needs at least one scene. Owning the window also means keeping a reference to it, so the second click on Settings brings back the same window instead of opening another.

Borrowing A Dock Icon While A Window Is Open#

You can also change the activation policy at runtime. setActivationPolicy(_:) has accepted any policy since OS X 10.9 and returns whether it worked. Switching to .regular while Settings is open gives the app a Dock icon and its own menu bar, like any other app with a window up. Switch back when the window closes:

Swift
func showSettings() {
    NSApp.setActivationPolicy(.regular)
    NSApp.activate()
    settingsWindow.makeKeyAndOrderFront(nil)
}

// NSWindowDelegate
func windowWillClose(_ notification: Notification) {
    NSApp.setActivationPolicy(.accessory)
}

If the app can have more than one window open, only switch back when the last one closes, or the Dock icon disappears while a window is still up. In an app where I let people turn the Dock icon on and off, setting .accessory is followed by NSApp.activate(ignoringOtherApps: true) on the next run loop pass, so an open Settings window stays in front.

None of our shipping apps flip the policy for Settings. Activating the app plus makeKeyAndOrderFront(nil) covers it, and on a macOS 14 target that’s NSApp.activate() followed by window.makeKeyAndOrderFront(nil) in the menu item’s action.