October 3, 2020

macOS context menu from an NSButton

Currently I am developing a small utility app for the Mac as a side project, and one of the requirements is the ability to open a URL in a browser. The idea was to have a button that when pressed displays a context menu with several options which the user can select from. Kind of like a drop down menu but visually different.

I am not an expert in macOS development and I am very inexperienced when it comes to AppKit, thus what seemed like a simple task actually took me some time. So for the sake of helping out another poor soul who might be struggling like I was, I am going to show you how I managed to solve my problem. In the end the solution is extremely simple.

The Button

So, one of the UI elements that is in the app is a Button, an NSButton to be exact that when pressed should reveals a Context menu with several options.

Of course you are free to do this how ever you prefer but just note that I used a StoryBoard and connected the NSButton action to my NSViewController visually from the StoryBoard.

The result looks like this, a very simple IBAction in my NSViewController.

@IBAction func onButtonPressed(_ sender: NSButton) {
    // More code to come
}

The menu

So lets look at how to create a context menu, or more correctly an NSMenu in code from our action. Start by declaring a property at the class level. We will declare it as an Implicitly Unwrapped Optional

private var openMenu: NSMenu!

You will also need to declare an Enum to handle the menu item selection using the tag property in the NSMenuItem you instantiate.

enum MenuItemTags: Int {
    case optionOne = 1
    case optionTwo
    case optionThree
}

The enum is totally optional, you can easily just hard code different integers for the tag property if you prefer.

Next, in our IBAction we will take care of instantiating the NSMenu and we will add a couple of NSMenuItem with the help of a helper function.

@IBAction func onButtonPressed(_ sender: NSButton) {
    self.openMenu = NSMenu()

    self.openMenu.addItem(contextMenuItem("Option 1", tag: MenuItemTags.optionOne.rawValue))
    self.openMenu.addItem(contextMenuItem("Option 2", tag: MenuItemTags.optionTwo.rawValue))
    self.openMenu.addItem(contextMenuItem("Option 3", tag: MenuItemTags.optionThree.rawValue))

    // We will show the menu here
}


private func contextMenuItem(_ title: String, tag: Int) -> NSMenuItem {
    let menuItem = NSMenuItem(title: title,
                              action: #selector(MyViewController.handleMenuItemSelected),
                              keyEquivalent: "")
    menuItem.tag = tag
    menuItem.target = self // IMPORTANT, otherwise it looks into the responders chain
    return menuItem
}

Please note that you will need to replace MyViewController.handleMenuItemSelected with your own selector. Also note that unless you plan to handle the menu item selection via the Responders chain, you need to set the target to self otherwise your menu item will stay grayed out.

Display the context menu

The simplest way to display an NSMenu is to use the following instance method, as can be found in the developer documentation.

nsmenu popup

Thus the complete code for the NSViewController looks like so, including a demo menu item selection handler.

import Foundation
import AppKit

enum MenuItemTags: Int {
    case optionOne = 1
    case optionTwo
    case optionThree
}

class MyViewController : NSViewController {

    private var openMenu: NSMenu!

    override func viewDidLoad() {
        // Initialization code goes here...
    }

    @IBAction func onButtonPressed(_ sender: NSButton) {
        self.openMenu = NSMenu()

        self.openMenu.addItem(contextMenuItem("Option 1", tag: MenuItemTags.optionOne.rawValue))
        self.openMenu.addItem(contextMenuItem("Option 2", tag: MenuItemTags.optionTwo.rawValue))
        self.openMenu.addItem(contextMenuItem("Option 3", tag: MenuItemTags.optionThree.rawValue))

        let location = NSPoint(x: 0, y: sender.frame.height + 5) // Magic number to adjust the height.
        self.openMenu.popUp(positioning: nil, at: location, in: sender)
    }

    private func contextMenuItem(_ title: String, tag: Int) -> NSMenuItem {
        let menuItem = NSMenuItem(title: title,
                                action: #selector(MyViewController.handleMenuItemSelected),
                                keyEquivalent: "")
        menuItem.tag = tag
        menuItem.target = self // IMPORTANT, otherwise it looks into the responders chain
        return menuItem
    }

    @objc
    func handleMenuItemSelected(_ sender: AnyObject) {
        guard let menuItem = sender as? NSMenuItem else { return }

        switch menuItem.tag {
        case MenuItemTags.optionOne.rawValue:
            // User selected Option 1, do something awesome!
        case MenuItemTags.optionTwo.rawValue:
            // User selected Option 2, do something awesome!
        case MenuItemTags.optionThree.rawValue:
            // User selected Option 3, do something awesome!
        }
    }

Conclusion

As you can see by the code above, it is very easy to display a context menu when your UI requires it. By using the NSMenu::popUp(positioning:at:in:) instance method we can specify exactly where we want the NSMenu to appear.

Copyright Ⓒ 2020 Christian Giacomi