Search code examples
macosswiftuiappdelegate

Open a window from menu bar (SwiftUI, MacOS)


I started working on an app for MacOS and got stuck on several issues. One of them is the following.

I have a UIElement app.

It's initialized in the following way:

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

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

In my AppDelegate.swift I have:

import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
    static private(set) var instance: AppDelegate!
    var settingsWindow: NSWindow!
    lazy var statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    let menu = AppMenu()
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        statusBarItem.button?.image = NSImage(
            systemSymbolName: "circle",
            accessibilityDescription: "My App"
        )
        statusBarItem.menu = menu.createMenu()
    }

    @objc func openSettingsWindow() {
        if settingsWindow == nil {
            let settingsView = SettingsView()
            settingsWindow = NSWindow(
                contentRect: NSRect(x: 20, y: 20, width: 480, height: 300),
                styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
                backing: .buffered,
                defer: false
            )
            settingsWindow.center()
            settingsWindow.title = "Settings"
            settingsWindow.isReleasedWhenClosed = false
            settingsWindow.contentView = NSHostingView(rootView: settingsView)
        }
        settingsWindow.makeKeyAndOrderFront(nil)
    }
}

And the last, the menu is built like this:

import SwiftUI

class AppMenu: NSObject {
    let menu = NSMenu()

    func createMenu() -> NSMenu {
        // ...
        let settingsMenuItem = NSMenuItem(title: "Settings", action: #selector(settings), keyEquivalent: ",")
        settingsMenuItem.target = self
        menu.addItem(settingsMenuItem)
        // ...
        return menu
    }

    @objc func settings(sender: NSMenuItem) {
        NSApp.sendAction(#selector(AppDelegate.openSettingsWindow), to: nil, from: nil)
    }
}

But when I open the app in the menu bar and hit Settings the window with SettingsView is displayed randomly (I'm not sure what is exactly the reason for it being opened and not).

In the log section of Xcode I see warnings:

2023-05-29 21:39:57.172266+0200 My App[2211:1474025] [Window] Warning: Window NSMenuWindowManagerWindow 0x7fb8d4e21640 ordered front from a non-active application and may order beneath the active application's windows.
2023-05-29 21:40:02.197549+0200 My App[2211:1474025] [Window] Warning: Window NSWindow 0x7fb8d3717790 ordered front from a non-active application and may order beneath the active application's windows.

The first warning is fired when I hit the app icon in the menu bar (every time) and the second – when Settings is clicked.

My goal is to have an app without a window in docker but with an opportunity to open different windows via its menu bar items.


Solution

  • I think your settings window is opening each time, but calling from your status bar menu does not activate your app. Thus, it's hidden under other windows.

    Option 1: Activate your app

    settingsWindow.makeKeyAndOrderFront(nil)
    
    NSApp.activate(ignoringOtherApps: true) // add this
    

    Option 2: Raise window level

    settingsWindow.level = NSWindow.Level.init(rawValue: 1)  // add this
    
    settingsWindow.makeKeyAndOrderFront(nil)
    

    If you want an app without Dock icon, add this in your Info.plist:

    LSUIElement = TRUE