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.
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
}
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.
The simplest way to display an NSMenu
is to use the following instance method, as can be found in the developer documentation.
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!
}
}
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.