41

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
  • Could you clarify where exactly in the AppDelegate this code goes? I'm getting crashes and status bar icons that work temporarily, but disappear quickly. Commented Nov 28, 2020 at 1:19
  • Where does it crash exactly? Here "NSStatusBar.system"? Commented Nov 28, 2020 at 22:36
  • Yep -- I solved it though. I misunderstood your answer and thought the NSStatusBar was something already created and provided by the system. Commented Nov 28, 2020 at 23:54

3 Answers 3

35

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.

Edit2: Added an additional piece of code that allows using a SwiftUI view as the status bar icon. Might be handy for displaying dynamic badges.

Found a way to show this in without an annoying NSPopover. Even though I used AppDelegate's applicationDidFinishLaunching to execute the code, it can be called from any place of your app, even in a SwiftUI lifecycle app.

Here is the code:

func applicationDidFinishLaunching(_ aNotification: Notification) {
        // SwiftUI content view & a hosting view
        // Don't forget to set the frame, otherwise it won't be shown.
        //
        let contentViewSwiftUI = VStack {
            Color.blue
            Text("Test Text")
            Color.white
        }
        let contentView = NSHostingView(rootView: contentViewSwiftUI)
        contentView.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
        
        // Status bar icon SwiftUI view & a hosting view.
        //
        let iconSwiftUI = ZStack(alignment:.center) {
            Rectangle()
                .fill(Color.green)
                .cornerRadius(5)
                .padding(2)
                
            Text("3")
                .background(
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 15, height: 15)
                )
                .frame(maxWidth: .infinity, maxHeight: .infinity,  alignment: .bottomTrailing)
                .padding(.trailing, 5)
        }
        let iconView = NSHostingView(rootView: iconSwiftUI)
        iconView.frame = NSRect(x: 0, y: 0, width: 40, height: 22)
        
        // Creating a menu item & the menu to add them later into the status bar
        //
        let menuItem = NSMenuItem()
        menuItem.view = contentView
        let menu = NSMenu()
        menu.addItem(menuItem)
        
        // Adding content view to the status bar
        //
        let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem.menu = menu
        
        // Adding the status bar icon
        //
        statusItem.button?.addSubview(iconView)
        statusItem.button?.frame = iconView.frame

        // StatusItem is stored as a property.
        self.statusItem = statusItem
    }

enter image description here

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you for this! Exactly what I was looking for. However, I can't figure out how to get any interactive elements in the view (buttons, TextFields) to work. Any thoughts?
@BradG. If you add a button to the content view it's interactable. In my case it prints data gist.github.com/kmalyshev/7ef834efaeed83014f6ba851582fca0d . Is there anything that blocks interaction in your case?
I created a minimal project to demonstrate. The TextField actually does work initially, but at some point stops accepting new input (typing anything in it closes the menu). Menu never works. github.com/bgreenlee/MinimalMenuBarApp
@BradG. Unfortunately I didn't got that far. Having some custom view was sufficient for me. There is a documentation on that topic. It says about limitations of such an approach. Perhaps there is a way to overcome limitations, handle mouse and keyboard events manually. developer.apple.com/library/archive/documentation/Cocoa/…
@eemrah These badges are usually rendered manually, there are examples on stackoverflow how to draw them with UIKit. However the same approach with a HostingView can be used for the icon. I updated the code, please take a look.
|
17

MenuBarExtra (macOS Ventura)

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. Use a MenuBarExtra when you want to provide access to commonly used functionality, even when your app is not active.

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 in the Dock

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

enter image description here

Or you can hide App's icon programmatically.

13 Comments

Thanks for the update. It took Apple some time to get to the status bar API. I'd like to add to your reply - it's possible to add any SwiftUI views to 'MenuBarExtra', not just buttons.
Do you have any idea on how we can set a max frame for a MenuBarExtra ? I already tried to use the defaultSize modifier but it seems that it's not working
When I use a custom SVG icon, it suffers from scaling issues - it is ugly and pixelated :(
@PavelLobodinský What did you end up doing for MacMenuBarExtra label? It's literally impossible to create a dynamic label without suffering from blurriness, odd scaling and certain views not being rendered at all. I think I'm going to just use the solution below instead of MacMenuBarExtra right now
@RahulBir I've ended up with the good old NSStatusItem.
|
11

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

6 Comments

Unfortunately the code you posted is an AppKit way to create those menus. It's not SwiftUI way nor it supports SwiftIUI views.
Well, if you create a project with AppKit App Delegate Life cycle, you still got an AppDelegate.
Yes, but it's not what I asked. I am interested in showing swiftUI in that top menu.
Happy it helped. I already voted to open it again because that is a really good and helpful question I was searching about aswell
I added an alternative solution without NSPopover. Perhaps you find it useful too.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.