Connecting Menu Items to Views in AppKit
I’ve been playing around with AppKit recently in an attempt to build something simple but useful. I’ve heard various things about AppKit as a UI framework. It’s not a new framework — and I do find the fact that I’m learning it at the same time it’s being replaced with SwiftUI a bit amusing — but there are a few things that I’m starting to like.
An example of one that I just learnt was how to get menu items and responders to work. Up until now I had no idea how I could connect the action of a menu item to anything other than the app delegate. Opening the code editor alongside the story board and Ctrl+dragging from the menu item action outlet did nothing if anything other than the app delegate was there. If I wanted to add a menu item that operated on something like a text view, the approach I took to date was:
- Add an
NSApplicationDelegateand link it to the menu,
- Within that method, get the content view controller from the current main window,
- Type cast it to the concrete view controller subclass that I’m using for the project,
- Do the thing.
This worked, but it was far from optimal (it actually sucked quite a bit). For one thing, adding new menu items meant adding more methods to app delegate, making it larger and more difficult to navigate. It also didn’t help with keeping methods on the relevant class: the methods were added to the app delegate but had nothing to do with the delegate itself.
But the biggest downside of this approach was that it left the menu item, enabled even when that text view was not in focus. Clicking the menu item wouldn’t crash the program, thanks to Swift’s nullability safety, but it’s far from a great user experience having a menu item that looks available but will do nothing when the user clicks it. I’ve since learnt about the validateMenuItem() method, but the prospect of modifying this method every time I wanted to add a new menu item that worked on a view did not sound appealing.
Part of me was wondering if there was a better way to do this, but I’m afraid to say that I didn’t think much about what that would be. I found a solution that worked, sort of, so I accepted it as “just the way to do this” and moved on to other things.
It was only by accident, when investigating the events in the First Responder connection explorer, that a better approach revealed itself to me. I knew it was possible to connect actions of menu items to the First Responder object in the storyboard, but I didn’t know that the list of First Responder actions was dynamic. I just imagined that there was this massive list of all the possible actions anyone will possibly need defined somewhere within the depths of AppKit, and that these were all the actions that the First Responder object would know about. It was only today that I discovered that this was not the case, and that by adding a new
IBAction method to a subclass of a view will add it as a new connection of the First Responder.
I gave this a try. I defined a new
@IBAction method on a view subclass, and connected a menu item’s action to the associated outlet that appeared within the First Responder. Sure enough, clicking that menu item invoked that method. Even better, if that view was no longer in focus, the menu item was automatically disabled.
Here’s the technique in full:
- Identify the scene element that should be the responder of the action.
- Add a new
@IBActionmethod to the class of that scene element. This will probably be dependent on what makes sense for the scope of the action. For example, if the action makes sense for a particular window or content view controller, add the action to the relevant controller. If it makes sense for a particular view, like a text field, then create a subclass of that view and add the action to that subclass (delegates will not work, as far as I can tell). Don’t forget to set that as the class of the view within the storyboard.
- Check to see if that action appears as an outlet in the connection explorer of the First Responder.
- Connect the action outlet of the menu item to the corresponding outlet on the First Responder object.
I’m really happy I’ve found this technique. No longer needing to go through the app delegate whenever I want a menu item to operate on a view dramatically simplifies everything. It’s possible that if I just did some more reading on this I would have found this technique anyway. After all, a framework as old as AppKit will probably not stick with such an inefficient approach to doing this. The whole responder chain thing is something that I may need to read up a bit more, but I’ve found that it also helps in my learning when I bumble along a bit and knock into the answer like I did today.