Changing macOS menu item states with Swift
Before I begin, I hope there is a more straightforward solution, but this works. I say that because my macOS developer skills are not as good as my iOS ones. So if you know a better way, please comment and help a developer out, thanks.
The Problem
I have a macOS application with two menu items that I use to switch between different views. There is a checkmark next to the current view on the menu, and I need to change that state when the view switches and vice versa.
My Solution
I thought this would be a lot easier than it turned out to be; to put it another way, it took a lot of experimentation and research, far more than I expected.
Assign the NSMenu to an @IBOutlet
In _AppDelegate.swift_I created an IBOutlet and then connected the menu on the storyboard to it
@IBOutlet weak var mainMenu: NSMenu!
OK, that gave me a way to talk with the menu from across my application. Given how much everyone talks about not putting things in AppDelegate I found this strange, but I’m going along with it since it worked.
Now the real fun begins within the viewController
OK, now it was time to create the functions that get called when you click on the NSMenuItem
So for this, I need two, and I hooked them up the usual way using a storyboard by CTRL+Click or right-click and drag from the menu item to the function.
Here’s the content of those functions
// MARK: Menu Items
@IBAction func showSettingsWindow(_ sender: NSMenuItem) {
tabContainer.selectTabViewItem(at: 1)
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
}
@IBAction func showPostCreatorWindow(_ sender: NSMenuItem) {
tabContainer.selectTabViewItem(at: 0)
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
}
So what’s going on here?
Well, the first line tabContainer.selectTabViewItem(at: 1)
switches tabs in a view, not part of what we need to be concerned about for the menu.
Next up, since the first thing we want to do is toggle the checkmark for the item we clicked, we can take advantage of using the sender
. We toggle the state with a ternary, so off is now on and vice versa.
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
That’s the simple part.
Now we need to tell the other menu item to turn off, this is where I do not feel comfortable with my solution, but I’m going to offer a couple of different ways, all use the @IBOutlet in the AppDelegate.
Starting with the scenario where I click on the ‘Settings’ option, I need to turn off the ‘Post Creator’ menu option.
The first way is to access the menu item using the index of both the top-level menu item and the sub-item.
// 1 = File menu
// 0 = Post Creator item
mainMenu.items[1].submenu?.items[0].state = NSControl.StateValue.off
The second way is to use a tag that I have assigned to the ‘Post Creator’ menu item.
mainMenu.item(withTitle: "File")?.submenu?.item(withTitle: "Post Creator")?.state = NSControl.StateValue.off
A third way is to assign a tag to the menu items and use the tag to change the item state.
// 100 = Post Creator
mainMenu.items[1].submenu?.item(withTag: 100)?.state = NSControl.StateValue.off
The technique is the same for when I switch from the ‘Settings’ view back to ‘Post Creator’. So the complete code for both actions would be this.
// MARK: Menu Items
@IBAction func showSettingsWindow(_ sender: NSMenuItem) {
tabContainer.selectTabViewItem(at: 1)
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
mainMenu.items[1].submenu?.item(withTag: 100)?.state = NSControl.StateValue.off
}
@IBAction func showPostCreatorWindow(_ sender: NSMenuItem) {
tabContainer.selectTabViewItem(at: 0)
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
mainMenu.items[1].submenu?.item(withTag: 101)?.state = NSControl.StateValue.off
}
The Wrap
So there it is, three different ways to access a macOS menu item. As mentioned previously, I am open to better ways to do this. Or is this how we do it, and it is just that awkward?