I am writing a little status bar app, which includes a text input.
The app is added to the status bar as a NSMenuItem which holds a NSHostingViewController with the SwiftUI-View. The status bar menu is added through an App delegate inside SwiftUIs @main scene. I've set "Application is agent" in the info.plist to true and I have an empty scene as @main in SwiftUI.
Problem: When I try to edit text, in the status bar app, the text field is not clickable and does not receive text input. When I add a window, the text field in the status bar app works as expected as long as the window is in foreground.
From my understanding, this is caused by the text field not being inside a window marked as key window. Is there any workaround to make the text field work as expected without the need for an additional app window?
Minimal example for the app delegate:
class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let statusBarButton = statusItem?.button {
statusBarButton.image = NSImage(systemSymbolName: "6.circle", accessibilityDescription: "")
statusBarButton.image?.isTemplate = true
}
let menuItem = NSMenuItem()
menuItem.view = createView()
let menu = NSMenu()
menu.addItem(menuItem)
statusItem?.menu = menu
}
private func createView() -> NSView {
let view = HostingView(rootView: ContentView())
view.frame = NSRect(x: 0, y: 0, width: 520, height: 260)
return view
}
}
SwiftUI @main
import SwiftUI
import AppKit
@main
struct MacApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings {
EmptyView()
}
}
}
Edit: I figured out, that there are two windows registered for my app. One for the status bar icon and one for the menu. However, the code below does not result in the menu window to become the key window. (although it is in fact the only window which returns true for .canBecomeKey)
.onAppear {
let window = NSApp.windows.first { $0.canBecomeKey }
window?.makeKey()
}
Second Edit: Apparently, the problem is not, that the window is not able to become key, but that the app is not set as the active app when the status bar menu is opened. I came up with this rather ugly hack, which causes the menu to close and re-open when another app was in focus before, but at least buttons and textfields work as expected with this:
func applicationDidFinishLaunching(_ notification: Notification) {
...
// Also let AppDelegate implement NSMenuDelegate
menu.delegate = self
...
}
func menuWillOpen(_ menu: NSMenu) {
#warning("This is a hack to activate the app when the menu is shown. This will cause the menu to close and re-open when another window was focused before.")
if !NSApp.isActive {
NSApp.activate(ignoringOtherApps: true)
DispatchQueue.main.async {
self.statusItem?.button?.performClick(nil)
}
}
}
I am still looking for a proper solution.