macOS Status Bar apps: Submenus

macOS Status Bar apps: Submenus

April 2, 2023 | Previous part | Next part |

To create a submenu, you will be creating an NSMenuItem and adding a submenu to it. This submenu is another instance of an NSMenu, which you then add items to. In part 1, we added NSMenuItems to the NSMenu using the .addItem method, seen here:

statusBarMenu.addItem(
    withTitle: "hi",
    action: #selector(AppDelegate.hello),
    keyEquivalent: "h"
)

However, we did not explicitly say we were adding NSMenuItems, we added them using the NSMenuItem initializer:

init(title: String, action: Selector?, keyEquivalent: String)

Architecturally It may look like this:

- mainMenuBar = NSMenu()
- subMenuBar = NSMenuItem()
- - subMenuBar.title = "Main first"
- - subMenuBar.submenu = NSMenu()
- - subMenuBar.submenu?.addItem(NSMenuItem(withTitle: "first", action: #selector(), keyEquivalent: "")
- - subMenuBar.submenu?.addItem(NSMenuItem(withTitle: "second", action: #selector(), keyEquivalent: "")
- - subMenuBar.submenu?.addItem(NSMenuItem(withTitle: "third", action: #selector(), keyEquivalent: "")
- mainMenuBar.addItem(subMenuBar)
- mainMenuBar.addItem(NSMenuItem(withTitle: "Main second", action: nil, keyEquivalent: ""))
- mainMenuBar.addItem(NSMenuItem(withTitle: "Main third", action: nil, keyEquivalent: ""))

If you do set up a submenu, you can also insert items into it by referring to the index of the menu item. You can also choose to insert into a position explicitly like so:

statusBarMenu.item(at: 0)?
    .submenu?.insertItem(
        withTitle: "Sub-m",
        action: nil,
        keyEquivalent: "h",
        at: 3
    )

Code example

import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    let statusBarMenu = NSMenu()
    var statusBarItem: NSStatusItem!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let statusBar = NSStatusBar.system

        statusBarItem = statusBar.statusItem(
            withLength: NSStatusItem.variableLength
        )

        statusBarItem.button?.image = NSImage(named: NSImage.Name("NXdefaulticon"))

        statusBarItem.menu = statusBarMenu

        let vmStatusMenu = NSMenuItem()
        vmStatusMenu.title = "VM Status"
        vmStatusMenu.image = NSImage(named: "NSAppleMenuImage")

        vmStatusMenu.submenu = NSMenu()
        vmStatusMenu.submenu?.addItem(NSMenuItem(title: "Start", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))
        vmStatusMenu.submenu?.addItem(NSMenuItem(title: "Stop", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))
        vmStatusMenu.submenu?.addItem(NSMenuItem(title: "Restart", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))
        vmStatusMenu.submenu?.addItem(NSMenuItem.separator())
        vmStatusMenu.submenu?.addItem(NSMenuItem(title: "About the app", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))

        statusBarMenu.addItem(vmStatusMenu)

        statusBarMenu.addItem(
            withTitle: "Settings",
            action: #selector(AppDelegate.hello),
            keyEquivalent: ","
        )

        statusBarMenu.addItem(
            withTitle: "Quit",
            action:  #selector(AppDelegate.quit),
            keyEquivalent: "q"
        )
    }

    @objc func hello() {
        print("Hi world")
    }

    @objc func quit() {
        print("Shutting down")
        NSApplication.shared.terminate(self)
    }
}

func applicationWillTerminate(_ aNotification: Notification) {}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }