How to create Status Bar icon and menu in macOS using SwiftUI

Viewed 6744

What is SwiftUI API for creating status bar menus?

Apple seems to use SwiftUI views in Battery & WiFi menus according to the accessibility inspector. Screenshot of a battery menu attached, also its view hierarchy.

Battery menu screenshot View hierarchy of the battery menu

EDIT:

Posted the solution as a separate answer.

3 Answers

Since this question received more attention lately, and the only reply doesn't fully solve the issue I would like to repeat the edited part of my question and mark it as resolved.

Found a way to show this in without an annoying NSPopover. You'd need to have AppDelegate or NSApplicationDelegateAdaptor in case you use SwiftUI App lifecycle. Then you create NSMenu and add NSMenuItem that can have a custom view.

Here is the code:

let contentView = VStack {
    Text("Test Text")
    Spacer()
    HStack {
        Text("Test Text")
        Text("Test Text")
    }
    Spacer()
    Text("Test Text")
}

let view = NSHostingView(rootView: contentView)

// Don't forget to set the frame, otherwise it won't be shown.
view.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
        
let menuItem = NSMenuItem()
menuItem.view = view
        
let menu = NSMenu()
menu.addItem(menuItem)
        
// StatusItem is stored as a class property.
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
self.statusItem?.menu = menu
self.statusItem?.button?.title = "Test"

enter image description here

Inside the AppDelegate add the following code:

// Create the status item in the Menu bar 
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))

// Add a menu and a menu item
let menu = NSMenu()
let editMenuItem = NSMenuItem()
editMenuItem.title = "Edit"
menu.addItem(editMenuItem)

//Set the menu 
self.statusBarItem.menu = menu

//This is the button which appears in the Status bar
if let button = self.statusBarItem.button {
    button.title = "Here"
}

This will add a Button with a custom Menu to your MenuBar.

enter image description here

Edit - How to use SwiftUI View

As you asked, here is the answer how to use a SwiftUI View.

First create a NSPopover and then wrap your SwiftUI view inside a NSHostingController.

var popover: NSPopover


let popover = NSPopover()
popover.contentSize = NSSize(width: 350, height: 350)
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
    self.popover = popover

Then instead of showing a NSMenu, toggle the popover:

self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
     button.title = "Click"
     button.action = #selector(showPopover(_:))
}

With following action:

@objc func showPopover(_ sender: AnyObject?) {
    if let button = self.statusBarItem.button
    {
        if self.popover.isShown {
            self.popover.performClose(sender)
        } else {
            self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
    }
}

enter image description here

MenuBarExtra

In macOS 13.0+ and Xcode 14.0+, the MenuBarExtra struct allows you create a system menu bar, that is similar to NSStatusBar's icons and menus. MenuBarExtra's item will be displayed in the system menu bar when the specified binding is set to true.

enter image description here

import SwiftUI

@available(macOS 13.0, *)                       // macOS Ventura
@main struct StatusBarApp: App {
    
    @State private var command: String = "A"
       
    var body: some Scene {

        MenuBarExtra(command, systemImage: "\(command).circle") {
           
            Button("Uno") { command = "A" }
                .keyboardShortcut("U")
           
            Button("Dos") { command = "B" }
                .keyboardShortcut("D")
           
            Divider()

            Button("Salir") { NSApplication.shared.terminate(nil) }
                .keyboardShortcut("S")
        }
    }
}

Getting rid of the App's icon from the Dock

In Xcode's Info tab, choose Application is agent (UIElement) and set its value to YES.

enter image description here

Related