Chapters

Hide chapters

Catalyst by Tutorials

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

11. Barista Training: Toolbar
Written by Andy Pereira

Toolbars are an essential part of macOS applications. Without a doubt, NSToolbar is used so ubiquitously across so many apps that most users may overlook its presence. Because of this, it’s essential to understand what you get when you use a toolbar and how toolbars behave. By adopting NSToolbar in your app, you have access to almost two decades worth of work from the smart developers and designers at Apple.

Getting started

Open the starter project for this chapter. Select My Mac for the active scheme, and Build and run. At the moment, this project has a split view controller and can handle multiple windows:

Adding the toolbar

NSToolbar is a macOS-specific control. In the past, you probably had to use macros or targets to ensure frameworks did not get imported into unsupported builds. With Catalyst, you’ll need to be able to integrate your macOS, iOS and iPadOS code more seamlessly.

To use NSToolbar in this project, open BridgingHeader.h, and add the following lines of code at the bottom of the file:

#import <Foundation/Foundation.h>
#import <UIKit/NSToolbar+UIKitAdditions.h>

By importing NSToolbar+UIKitAdditions.h, you’ll be able to easily reference NSToolbar in UIKit code without worrying about compilation issues.

Next, to add the toolbar, open SceneDelegate.swift and add the following to the end of scene(_:willConnectTo:options:):

#if targetEnvironment(macCatalyst)
// 1
if let scene = scene as? UIWindowScene,
  let titlebar = scene.titlebar {
  // 2
  let toolbar = NSToolbar(identifier: "Toolbar")
  // 3
  titlebar.toolbar = toolbar
}
#endif

Here’s what you’ve done:

  1. You check that the scene has a titlebar. This property will be present if the app is running inside of a macOS environment.
  2. Then you create a toolbar with an identifier. Every one of the toolbars will have the same identifier so that the system synchronizes their state across windows.
  3. Last, you set the toolbar on the titlebar.

Build and run, and you’ll see a beautiful — albeit empty — toolbar.

It doesn’t make sense to keep the navigation bars around any longer, as the toolbar will serve the same purpose.

To get rid of both of the navigation bars, open MainTableViewController.swift and add the following to the end of viewDidLoad():

#if targetEnvironment(macCatalyst)
navigationController?.navigationBar.isHidden = true
#endif

Then, open EntryTableViewController.swift and do the same. Build and run, and now your navigation bars are gone, leaving just a toolbar:

Adding buttons

In its current state, the toolbar is providing no functionality to the app. To start adding functionality, you’ll be adding a few buttons.

Open SceneDelegate.swift and add the following after setting the toolbar on the titlebar:

toolbar.delegate = self

Next, at the end of the file, inside the empty macro that checks for Catalyst, add the following:

extension NSToolbarItem.Identifier {
  static let addEntry = 
    NSToolbarItem.Identifier(rawValue: "AddEntry")
  static let deleteEntry = 
    NSToolbarItem.Identifier(rawValue: "DeleteEntry")
  static let shareEntry =
    NSToolbarItem.Identifier(rawValue: "ShareEntry")
}

extension SceneDelegate: NSToolbarDelegate {

}

These three toolbar identifiers are needed to start adding buttons to the toolbar. Just like you added an identifier to the toolbar for the system to know how to sync across windows, these identifiers allow the toolbar to know what is added to itself.

Next, add the following to the NSToolbarDelegate extension:

func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar)
  -> [NSToolbarItem.Identifier] {
    return [.addEntry, .deleteEntry, .shareEntry, .flexibleSpace]
}

By adding this, you tell the toolbar which identifiers are allowed to be in the toolbar. You’ll notice that the first three identifiers are what you added above. The last one, .flexibleSpace, is a system-defined identifier that places a blank item that automatically adjusts its spacing.

Now, add the following method:

func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar)
  -> [NSToolbarItem.Identifier] {
    return [.addEntry, .flexibleSpace, .shareEntry]
}

Adding toolbarDefaultItemIdentifiers(_:) will inform the toolbar what should initially be displayed in itself. Also, later on, when you start customizing the toolbar, it will provide the user a way to reset the toolbar to the initial state.

You’re almost there, but there are a few more steps before you can add the buttons. Add the following methods inside the same extension:

// 1. 
func toolbar(_ toolbar: NSToolbar,
  itemForItemIdentifier itemIdentifier: 
  NSToolbarItem.Identifier,
  willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
    var item: NSToolbarItem? = nil
    return item
}

// 2. 
@objc private func addEntry() {
}
  
@objc private func deleteEntry() {
}

@objc private func shareEntry(_ sender: UIBarButtonItem) {
}

Here’s what you’ve added:

  1. This method will provide the actual buttons to a toolbar. It is currently incomplete and will be finished shortly.
  2. These methods are for convenience right now and will be finished later.

Since you’ll be adding three buttons, add the following in the extension to help reduce code duplication:

private func toolbarItem(
  itemIdentifier: NSToolbarItem.Identifier,
  barButtonItem: UIBarButtonItem,
  toolTip: String? = nil, label: String?) -> NSToolbarItem {
    // 1.
    let item = NSToolbarItem(itemIdentifier: itemIdentifier,
      barButtonItem: barButtonItem)
    // 2. 
    item.isBordered = true
    // 3.
    item.toolTip = toolTip
    if let label = label {
      // 4.
      item.label = label
    }
    return item
}

And here’s what this is doing:

  1. Create an NSToolbarItem with a UIBarButtonItem. It may seem a little crazy, but the system is actually taking care of inserting an item found within UIKit into something traditionally thought of as an AppKit object. This truly speaks to the capability of Catalyst. The bar button item passed will provide a system image for the buttons, which you’ll see shortly.

  2. Setting isBordered gives your button the traditional shape that you’re used to seeing in most default toolbars.

  3. The toolTip will provide a label when you hover your mouse over a button.

  4. To show a text label under the button, you must set label.

At last, you’re ready to add the buttons. Replace toolbar(_:itemForItemIdentifier:willBeInsertedIntoToolbar:) with the following:

func toolbar(_ toolbar: NSToolbar, 
  itemForItemIdentifier itemIdentifier: 
  NSToolbarItem.Identifier, 
  willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
  
  var item: NSToolbarItem? = nil
  if itemIdentifier == .addEntry {
    let barButtonItem =
      UIBarButtonItem(barButtonSystemItem: .add,
                      target: self,
                      action: #selector(addEntry))
    item = toolbarItem(itemIdentifier: .addEntry,
                       barButtonItem: barButtonItem,
                       toolTip: "Add Entry",
                       label: "Add")
    item?.target = self
    item?.action = #selector(addEntry)
  } else if itemIdentifier == .deleteEntry {
    let barButtonItem =
      UIBarButtonItem(barButtonSystemItem: .trash,
                      target: self,
                      action: #selector(deleteEntry))
    item = toolbarItem(itemIdentifier: .deleteEntry,
                       barButtonItem: barButtonItem,
                       toolTip: "Delete Entry",
                       label: "Delete")
    item?.target = self
    item?.action = #selector(deleteEntry)
  } else if itemIdentifier == .shareEntry {
    let barButtonItem =
      UIBarButtonItem(barButtonSystemItem: .action,
                      target: self,
                      action: #selector(shareEntry(_:)))
    item = toolbarItem(itemIdentifier: .shareEntry,
                       barButtonItem: barButtonItem,
                       toolTip: "Share Entry",
                       label: "Share")
  }
  return item
}

You add custom buttons to the toolbar by checking the identifier passed in and returning the appropriate button. Here are the buttons you added:

  1. Add: This will add a new journal entry to the app.
  2. Delete: Based on the active window, it will delete the selected entry.
  3. Share: Just like delete, it will share the active window’s current entry.

Build and run. Because your default buttons are just Add and Share, you will only see two buttons initially:

OK. You’re now officially past the hardest part of working with toolbars in this tutorial!

Customizing the toolbar

Toolbars don’t always have to contain a fixed set of buttons. Above, you provided a delete button without giving the user a way to see it. You can enable your toolbar to be customized by the user, and save its state between launches.

Still within SceneDelegate.swift, add the following lines of code after you set the delegate for the toolbar inside of scene(_:willConectTo:options:):

toolbar.allowsUserCustomization = true
toolbar.autosavesConfiguration = true

By setting allowsUserCustomization, you enable users to customize their toolbar by right-clicking on it. Also, autosavesConfiguration will determine if the system should save the toolbar configuration to NSUserDefaults, persisting the user’s preferences between runs.

Build and run, then right-click on your toolbar. You’ll see an option to Customize Toolbar:

Select this option, and you’ll see a modal window in which you can customize the toolbar:

Try changing the configuration, then quit the application and restart it. Your changes should remain between runs.

If you allow user customization, you should set the label for the items in the customization palette. You usually can make this the same as the label in the toolbar. Add the following to toolbarItem(itemIdentifier:barButtonItem:toolTip:label:), just after setting the item’s label in the if let block:

item.paletteLabel = label

Finally, you can also remove the title bar from the window, making the toolbar a bit smaller. To test this, add the following to scene(_:willConnectTo:options:), after setting the delegate:

titlebar.titleVisibility = .hidden

Build and run to see how this looks:

Notice that the labels and the title in the window are now gone. You can remove this line of code for the remainder of the tutorial.

Responding to actions

The last thing you need to do is respond to actions in the toolbar. To add items to the list, add this implementation to addEntry():

DataService.shared.addEntry(Entry())

Build and run, then select Add. You should see entries get added to the list:

Next, implement deleteEntry() with the following:

guard let splitViewController =
  window?.rootViewController as? UISplitViewController,
  let navigationController =
  splitViewController.viewControllers.first
    as? UINavigationController,
  let mainTableViewController =
  navigationController.topViewController
    as? MainTableViewController,
  let secondaryViewController =
  splitViewController.viewControllers.last
    as? UINavigationController,
  let entryTableViewController =
  secondaryViewController.topViewController
    as? EntryTableViewController,
  let entry = entryTableViewController.entry,
  let index = DataService.shared.allEntries
    .firstIndex(of: entry) else { return }
DataService.shared.removeEntry(atIndex: index)
mainTableViewController.selectEntryAtIndex(index)

This isn’t as complicated as it may first appear. Because your app can have multiple windows, you need to check which entry is selected in the window you select Delete in. This code simply goes through the hierarchy of the active window and removes the appropriate entry.

Build and run. Ensure you have the delete button in your toolbar, add a few entries, then select Delete.

Finally, implement shareEntry(_:) with the following:

guard let splitViewController =
  window?.rootViewController as? UISplitViewController,
  let navigationController =
  splitViewController.viewControllers.last
    as? UINavigationController,
  let entryTableViewController =
  navigationController.topViewController
    as? EntryTableViewController else {
    return
}
entryTableViewController.share(sender)

Like deleteEntry, you need to check the proper window. However, you simply call share(_:) on the EntryTableViewController, and the rest is taken care of. Because you used a UIBarButtonItem to back the NSToolbarItem, the code for handling a popover within share(_:) will work without any changes.

Build and run, add some text to an entry, then select Share.

Key points

  • Use Mac style toolbars, not iOS navigation bars in macOS apps.
  • Toolbars are for the entire window, not just the specific view controller presented to the user.
  • You can take advantage of built in toolbar items, with default images, or create your own.
  • Users are used to customizing toolbars in many apps. Ensure you provide this capability, as it makes sense.

Where to go from here?

This chapter showed you how quick it is to implement a macOS-centric design in a way that was never so easy. While knowing how to implement your own toolbar items is important, don’t forget there are several other system-provided toolbar items provided that you can put to use as well.

You can learn more about these topics from Apple’s website at https://developer.apple.com/documentation/appkit/nstoolbaritem/identifier.

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.