When I struggle to do seemingly simple things in Xcode, I remind myself that there are some Mac users for whom working in Xcode is almost all that they ever do on a Mac. So what appears alien to a dabbler like me is probably sleek and efficient for a pro.
One task which is often hard to get off pat is adding a new menu command, or repurposing an existing one. It’s not something that I do every day, and when I have to come back to it, I seem to struggle every time until the knack returns. This article not only looks at doing that with Interface Builder, but takes as an example a command which, in a single step, copies the contents of a textbox into the pasteboard.
For this sort of simple purpose, using the pasteboard is amazingly easy. As I have several menu commands which need to do this, I start with a common function for them all: given a pasteboard and a String, clear the existing pasteboard contents and write a string in their place. This is simplicity itself:
func writeToPasteboard(pasteboard: NSPasteboard, string: String) {
if string != "" {
pasteboard.clearContents()
pasteboard.writeObjects([string as NSPasteboardWriting])
} }
To be accessible as a sent action from a menu command, I declare my handling function as an IBAction. It then gets the String from one of the textboxes, and calls writeToPasteboard() to write that string to the General pasteboard:
@IBAction func copyPath(_ sender: AnyObject) {
let theStr = pathTextbox.stringValue
writeToPasteboard(pasteboard: NSPasteboard.general, string: theStr)
}
I then built myself a set of six of these IBActions, one to handle each of my new menu commands, in the window’s ViewController. In Interface Builder, I added a Submenu Menu Item named Copy…, and put all six sub-commands into that item.
Each then has to be wired up to call its respective IBAction, and this is normally where I get hopelessly confused. Logic would say that all you have to do is Control-drag from a menu command to a space in your ViewController source, and you should be able to make a new linked IBAction. But that isn’t the way that things work here.
Instead, handling these new menu commands has to go through the responder chain until it reaches the IBAction in my ViewController. To set that up in Interface Builder, open the new sub-menu and Control-drag from the menu command to the orange First Responder icon in the header of the Application Scene. A dark grey window should then pop up, listing all the available actions, from which I select copyPath:
Now when I right-click on the menu item, the window which appears should show this as a Sent Action, linked to the First Responder’s command.
This should also appear in the Connections Inspector.
If you now build your app, chances are that your new menu command will be shown in the menu, but disabled. The final and crucial step is to override the validateMenuItem() function, which tells macOS which menu items should be enabled. Here, I perform the simple test to check that the string about to be copied is not empty:
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
switch menuItem.action {
case #selector(ViewController.copyPath)?:
return pathTextbox.stringValue != ""
In other cases, you may simply want to return true, as the command is functional whenever this window is open:
case #selector(ViewController.saveCommand)?:
return true
Then, most importantly, set the default to call the normal routine, so that this doesn’t break anything else:
default:
return super.validateMenuItem(menuItem)
} }
None of this is difficult, but it is sufficiently counter-intuitive as to catch me out every time I come back to do it.