Chapters

Hide chapters

Catalyst by Tutorials

Third Edition · iOS 15 · Swift 5.6 · Xcode 13.3

10. Barista Training: Menu Bar
Written by Marin Bencevic

Welcome to barista training! In the next few chapters, you’ll learn all about adding different kinds of bars to your Catalyst app, including the menu bar, toolbars and supporting the touch bar. In this chapter, you’ll trim the default menu bar of the Journalyst app to remove some unnecessary items. You’ll also add new items to delete, share and add new entries. Get out your beans and your fancy hipster oat milk — it’s time to get brewing!

A Free Menu Bar

All Catalyst apps include a default menu bar for free. Open the starter project from the provided materials and run it on macOS. When your app is active, you’ll see its menu bar at the top of your screen. All the standard menus like File, Edit, etc. are already there.

Note: WWDC 2021 introduces a new property in info.plist called UIApplicationSupportsPrintCommand. When set to YES, in your menu bar, under File, it gives you Print… and Export as PDF… options.

Apple sometimes calls the menu bar the “main menu.” That’s how you must think about what actions to put in the menu bar. Like in a video game, the main menu includes general actions the user can perform inside your app.

One thing you must keep in mind is that menu bars aren’t dynamic. They’re built once when the app launches and the items never change during runtime. For more dynamic actions related to specific parts of your app, use context menus as described in Chapter 5, “Adding Some Context.”

The menu bar itself is a nested UIMenu instance. Each UIMenu can contain child menus and commands. Commands are the buttons you can press to do something. They can be enabled or disabled, and each command can specify a keyboard shortcut for easier access.

Commands get executed using something called the responder chain.

The Responder Chain

Open the Edit menu, and you’ll notice that Cut, Copy and Paste are all greyed out. This makes sense: Since nothing is selected, there’s nothing to cut or copy.

Disabled items in Edit menu.
Disabled items in Edit menu.

However, if you type in something in the entry screen and select the text, you’ll notice the menu items are now enabled.

UIKit disables and enables these items for you using the responder chain. The responder chain is built of UIResponder instances, which include all views, view controllers and the app itself.

When you click the text view, it becomes the first responder. The responder chain starts from the text view and moves up the view hierarchy through all superviews and view controllers, all the way to the app itself.

Menu items use the responder chain to enable or disable themselves. Each menu item has an associated selector. When the first responder changes, menu items query the whole responder chain to see if anyone can perform their selector. In the case of Cut, the text view can perform the action. File ▸ Close is always active because the app handles that one, and the app is always at the far end of the responder chain.

OK, that’s enough theory for one day. It’s time to get hacking.

Beyond the Default Menu

You’ll start by trimming some unnecessary items from the menu bar. The menu bar can be changed either in Interface Builder or through code. In this section, you’ll use Interface Builder and, later in the chapter, you’ll learn how to do the same in code.

Open Main.storyboard, from the Library drag over a Main Menu anywhere on the storyboard. You see a new scene in the storyboard that looks just like your menu bar:

Main Menu in the storyboard.
Main Menu in the storyboard.

Select the Format menu in the Document Outline and press Command-Backspace to delete it. Since you’re not using rich text in the app, this menu serves no purpose. Build and run, and you see a menu bar without Format:

Menu bar without the format menu.
Menu bar without the format menu.

Now, add a new command for adding new entries. Start by going back to the Library and dragging over an Inline Section Menu to the top of the File menu in the Outline View.

Inline Section Menu in File menu.
Inline Section Menu in File menu.

This adds an inline menu with two commands. Inline menus can’t be opened. Instead, all their commands are added to the parent menu, separated with a thin line from other items.

Select Item 1 and open the Attributes inspector. Change the Title to New Entry. Then click the Key Equivalent box and press Option-Command-N. This adds a keyboard shortcut to the command.

Item 1 Attributes inspector.
Item 1 Attributes inspector.

Next, in RootSplitViewController.swift add the @IBAction attribute for addEntry(sender:) at the start of it’s declaration. It now looks like this:

@IBAction @objc private func addEntry(
  sender: UIKeyCommand
) {

Then return to the storyboard again, control-drag from the New Entry command to the first responder, which is the little yellow cube above the menu bar in the storyboard. Select addEntryWithSender: as the sent action.

New Entry's first responder.
New Entry's first responder.

This connects the addEntryWithSender: selector to the command. As discussed earlier, the system will search the responder chain until it finds an object which can perform that selector.

If the method weren’t marked as @IBAction, Interface Builder wouldn’t be able to find the method.

Build and run. Select File ▸ New Entry in the menu bar. You’ll see a new entry pop-up. You can also press Option-Command-N to add new items.

New entry in the File menu.
New entry in the File menu.

Congrats, young barista, you just brewed your first menu item. Keep the momentum going by adding a command to delete an entry.

Deleting Entries

Back in Main.storyboard, select Item 2 in the File submenu you added earlier. In the Attributes Inspector, change its Title to Delete Entry and set the Key Equivalent to Shift-Command-Backspace.

Open RootSplitViewController.swift and add @IBAction to removeEntry(sender:):

@IBAction @objc private func removeEntry(
  sender: UIKeyCommand
) {

Again, the method needs to be annotated with @IBAction so interface builder can see it.

Now, back in Main.storyboard control-drag from the Delete Entry command to the first responder and select removeEntryWithSender: as the action.

Build and run, then select the first entry. Select File ▸ Delete Entry to delete the entry.

Delete Entry in the File menu.
Delete Entry in the File menu.

It might be a good idea to give the user more information about which item they’re deleting. You can do that by changing the command’s title to include the date and time of the currently selected entry.

Open RootSplitViewController.swift and add the following override to the class, right after removeEntry:

// 1
override func validate(_ command: UICommand) {
  // 2
  switch command.action {
  case #selector(removeEntry):
    // 3
    if let mainNavigationController
         = viewController(for: .primary)
         as? UINavigationController,
       let mainTableViewController
         = mainNavigationController.topViewController
         as? MainTableViewController,
       let selectedIndexPath
         = mainTableViewController
         .tableView.indexPathForSelectedRow {
      // 4
      let entry
        = DataService.shared.allEntries[selectedIndexPath.row]
      command.title = "Delete \(entry.dateCreated)"
    } else {
      // 5
      command.title = "Delete Entry"
    }
  default:
    break
  }
}

This code is densely packed with information, so here’s a break-down of what’s going on:

  1. validate gets called for each command the view controller can perform actions for, and it gives you a chance to update the command’s look.
  2. You’re only interested in the command for deleting entries, so you check that its selector matches deleteEntry.
  3. Fetch the split view controller’s master, grab the main table view controller and its selected index path.
  4. If there’s a selected entry, change the command’s title to include the date and time of the entry.
  5. Otherwise, change its title back to Delete Entry.

Build and run. Open the File menu with an entry selected, you’ll see its date and time in the command’s title.

Delete with a title of date and time.
Delete with a title of date and time.

That’s a hot cup of freshly ground UX improvements served to your users!

Note: The responder chain can sometimes behave in unexpected ways and commands might get disabled for no apparent reason. You can combat this by calling becomeFirstResponder on the view you want to focus on, and even that is not guaranteed to always work. The responder chain can be quite fickle!

Next, you’ll add a command for sharing entries, but this time it’s not going to be in Interface Builder.

Sharing Entries

The menu bar is only one of the potentially many menus you can have in your app. Each UIResponder can add or remove items from their menus. The responder that’s responsible for the menu bar is the application itself, or in other words, the app delegate.

Open AppDelegate.swift and override the following method in the class:

override func buildMenu(with builder: UIMenuBuilder) {
}

This overrides a UIResponder method responsible for adding and removing items from menus. Since you’re only interested in changing the menu bar, add the following check to the method:

guard builder.system == .main else { return }

You’ll build a new inline menu that contains a command to share an entry. Start by adding the following code that creates the command to the end of the method:

let shareCommand = UIKeyCommand(
  title: "Share",
  action: #selector(EntryTableViewController.share),
  input: "s",
  modifierFlags: [.command]
)

You use a UIKeyCommand so that you can attach a keyboard shortcut to the command. This calls it Share and gives it a keyboard shortcut of Command-S. Once it’s pressed, the selector it calls is share implemented in EntryTableViewController.

Next, create the menu that contains the command by adding this bit of code to the end of the method:

let shareMenu = UIMenu(
  title: "",
  options: [.displayInline],
  children: [shareCommand]
)

Since the menu’s options say that it’s displayed inline, the title won’t get shown, so you can leave it empty. The menu only has one child: The share command you created earlier.

Finally, it’s time to use the builder to add the item to the menu bar. Add this line to the end of the method:

builder.insertChild(shareMenu, atStartOfMenu: .file)

You can specify exactly where your item goes. In this case, add it to the start of the File menu.

UIKit doesn’t call buildMenu if there’s an initial menu specified in the storyboard. To get around this issue, disable the main menu in the storyboard:

  1. Open Main.storyboard.
  2. Select Main Menu.
  3. In the Attributes Inspector uncheck Is Initial Menu.

Uncheck in Attributes inspector.
Uncheck in Attributes inspector.

This makes Main Menu not the main menu, and makes sure the app calls buildMenu at launch. Of course, the items you added in the storyboard won’t be visible anymore.

Build and run. Select an entry and type something in the log. Now select File ▸ Share to open the share sheet. You can also do this by pressing Command-S.

Share in File menu.
Share in File menu.

One final issue with the menu bar is that the Share item is enabled even if the log is completely empty. There’s no point in sharing an empty string, so you’ll disable the menu bar in that case.

Open EntryTableViewController.swift and override validate(_:) once again:

override func validate(_ command: UICommand) {
  switch command.action {
  case #selector(share):
    if textView.text.isEmpty {
      command.attributes = [.disabled]
    } else {
      command.attributes = []
    }
  default:
    break
  }
}

If the user hasn’t entered any text, you’re adding .disabled to the command’s attributes array. Otherwise, you’re clearing the attributes so that the command becomes enabled.

Note: You can also disable commands by overriding canPerformAction and returning false. Keep in mind that by doing so, validate won’t get called for the disabled command.

Build and run one last time and you’ll see Share disabled until you type something into the entry’s text view.

Disable Share in empty text view.
Disable Share in empty text view.

This concludes your barista training for the menu bar! Keep an eye out for any cafe managers bringing you job offers.

Key Points

  • Catalyst apps include a default menu bar for free.
  • The menu bar consists of nested menus which contain commands.
  • Each command has a selector that it calls when pressed, and uses the responder chain to enable and disable itself.
  • You add or remove items from the menu bar by dragging over a Main Menu to your app’s storyboard.
  • You can make the same changes in code by overriding buildMenu in the app delegate.
  • Override validate in a UIResponder subclass to change the appearance of a command.

Where to Go From Here?

The Human Interface Guidelines section on menus has some useful tips of which actions to consider for the menu bar, and where to put them.

To see which options you have when building out a menu bar, check out the documentation for UIMenuBuilder here. You can also see the different properties of UICommand to get a sense of how to further customize commands.

Finally, the WWDC 2019 session Taking iPad Apps for Mac to the Next Level walks you through building a menu bar using the menu builder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.