Home iOS & Swift Books Catalyst by Tutorials

12
Barista Training: The Touch Bar Written by Marin Bencevic

You’re just one final step away from becoming a true barista master! The one last remaining bar on your journey is… the Touch Bar. Whether or not you love the Touch Bar or think it’s a gimmick, many Mac users use it heavily. Because of this, supporting the Touch Bar is an important step in making your Catalyst app feel completely at home on macOS.

In this chapter, you’ll expand the app you’ve been working on to add a few useful items to the Touch Bar. You’ll learn about positioning those items and how to allow your users to customize them.

Before you get started, though, let’s talk a little about how the Touch Bar works under the hood.

Understanding the Touch Bar

While using your Mac, the Touch Bar is continuously changing depending on what’s active on the screen. Similarly to the menu bar, the Touch Bar uses the responder chain to determine which items to present. Take a look at Chapter 10, “Barista Training: Menu Bar” to learn more about the responder chain.

The gist is that each view controller and view is a responder, and one of them is the first responder, which is the currently active view. The responder chain works like a tree, going upwards from the first responder all the way to the root window of your app.

Each item in the responder chain can say “Here are the items I want in the Touch Bar.” When the first responder changes, the Touch Bar goes up the chain, picking up items as it goes. Of course, not all of these items fit on the Touch Bar, so the Touch Bar prioritizes items closer to the first responder. The ones at the back don’t get shown, and wait patiently for their turn to shine.

However, the ones in the back don’t necessarily have to take back stage! If you think an item is more or less important, you can set its priority to a higher or lower value. The Touch Bar will take this into account when ordering the items.

The responders suggest their items to the Touch Bar by overriding makeTouchBar. That method returns an NSTouchBar object. Don’t let the naming confuse you: The Touch Bar — the physical bar — displays multiple instances of NSTouchBar. In the following screenshot you’ll see four distinct NSTouchBar instances shown on the Touch Bar:

This is the Touch Bar of the Notes app. Bar 1 and 4 are system bars and they’re always there. Bar 3 is the bar of an active text field, which is currently the first responder. Bar 3 bullied through and hid some items from Bar 2 because Bar 2 is deeper in the responder chain.

Note: Since the Touch Bar is only available on macOS, NSTouchBar and related APIs are lifted directly from macOS and included in Catalyst, which explains the NS prefix. This means that already existing macOS-specific Touch Bar documentation and tutorials are generally applicable to Catalyst apps.

Adding items

Now that we’ve, ahem, touched on some theory, you’re ready to add some new items!

Open up the starter project from the provided materials and navigate to RootSplitViewController.swift. As mentioned before, each view controller is a responder and, since RootSplitViewController is always in the responder chain, it makes sense to add entry-related items there.

You’ll start by adding a button that adds a new entry. The first step to adding a Touch Bar item is to define its identifier. The Touch Bar uses these identifiers to keep track of which items to show and hide. It also uses the identifier to save customization options for specific items. You’ll read more about customization later in this chapter.

Add the following extension at the top of the file, right under the import:

#if targetEnvironment(macCatalyst)
extension NSTouchBarItem.Identifier {
  static let newEntry =
    NSTouchBarItem.Identifier(
    "com.raywenderlich.Journalyst.addEntry")
}
#endif

It’s a good practice to extend NSTouchBarItem.Identifier instead of peppering a bunch of hard-coded strings around your codebase.

Since the Touch Bar only exists on macOS, you’ll wrap most of the code from this chapter in a preprocessor macro that conditionally compiles the code only if it’s running on macOS.

Now you can create the item. As mentioned, each subclass of UIResponder can override makeTouchBar to add items to the Touch Bar.

Next, override makeTouchBar in the class like this:

#if targetEnvironment(macCatalyst)
override func makeTouchBar() -> NSTouchBar? {
  let bar = NSTouchBar()
  bar.defaultItemIdentifiers = [.newEntry]
  let button = NSButtonTouchBarItem(
    identifier: .newEntry,
    title: "New Entry",
    target: self,
    action: #selector(addEntry))
  bar.templateItems = [button]
  return bar
}
#endif

Here’s what you’re doing:

First, you create a new instance of NSTouchBar. The most important property of the bar is defaultItemIdentifiers — an array of all the items’ identifiers. If you forget to set this, the items won’t show.

Then, you create an NSButtonTouchBarItem object, which is a subclass of NSTouchBarItem. You define the button’s title and set its identifier to the one you just created. Just like menu bar items, Touch Bar items use the target-action pattern to determine what happens when tapped. Finally, you add the item to NSTouchBar’s templateItems property and return the bar. The templateItems property lets you directly manipulate which items the touch bar will show.

Build and run the app. You should see the item in the Touch Bar.

Note: If you’re running on a Mac without a Touch Bar, you can still test this out. In Xcode, choose Window ▸ Show Touch Bar and it will show the Touch Bar in a floating window.

Try clicking into the Journalyst Entry text field and note how this causes this item to disappear because it’s no longer in the responder chain. When you reactivate any cell in the list of entries, this will cause the “New Entry” item to reappear once again.

You’ll notice that you used a subclass of NSTouchBarItem. Generally, you won’t use the NSTouchBarItem class directly, since Apple provides a selection of pre-built item types for you. These include:

  • NSCandidateListTouchBarItem: Shows a list of options to pick from.
  • NSColorPickerTouchBarItem: Lets you pick a color.
  • NSSharingServicePickerTouchBarItem: Displays a list of ways to share provided data.
  • NSSliderTouchBarItem: Shows a slider between two values.
  • NSButtonTouchBarItem: That’s the one in the screenshot above. It displays a regular button.

You can also use NSCustomTouchBarItem to show a custom view in the Touch Bar item.

Unfortunately, at the time of writing, most of these items are exposed in a limited way in Catalyst, and several of them are completely unusable. Of the above items, the only fully usable ones as I write are NSButtonTouchBarItem and NSColorPickerTouchBarItem . Hopefully, this will change in future releases.

There are also higher-level subclasses that can contain multiple items. These are NSGroupTouchBarItem, which holds a group of items, and NSPopoverTouchBarItem, which expands to show more items when tapped. You’ll use NSGroupTouchBarItem later in the chapter.

Let’s get back to what you did in the code. You created your item by adding it to the bar’s templateItems property. This is the easiest way to create Touch Bar items. But it comes with a drawback. Because the Touch Bar has a direct reference to the item, it stays loaded in memory, even when not shown. That’s why you should use templateItems only for lightweight items.

Implementing the delegate

To avoid this memory issue, you’ll implement NSTouchBarDelegate. Instead of setting the items directly on the bar, you will only give the bar a list of item identifiers. The bar will then ask the delegate for the item only when it’s needed. This is similar to how table views work: Cells are created on-demand instead of being loaded automatically.

First, change the implementation of makeTouchBar. Remove the lines where you create and set the button on the bar, and add a new line to set the bar’s delegate to self. When finished, your method’s code should look like this:

let bar = NSTouchBar()
bar.delegate = self
bar.defaultItemIdentifiers = [.newEntry]
return bar

Next, at the bottom of the file add the following extension to implement the delegate:

#if targetEnvironment(macCatalyst)
extension RootSplitViewController: NSTouchBarDelegate {
  func touchBar(
  	_ touchBar: NSTouchBar,
  	makeItemForIdentifier identifier: NSTouchBarItem.Identifier)
  	-> NSTouchBarItem? {
  	
    switch identifier {
    case .newEntry:
      let button = NSButtonTouchBarItem(
        identifier: identifier,
        title: "New Entry",
        target: self,
        action: #selector(addEntry))
      return button
    default:
      return nil
    }
  }
}
#endif

This is similar to tableView(_:cellForRowAt:). The method asks the delegate to create an item based on the provided identifier. In the method, switch on the identifier and, if it matches the one you created earlier, create the item in the same way you did in makeTouchBar.

Build and run the app, and you should see the same item you saw earlier.

So, you just changed a bunch of your code, and absolutely nothing changed in the bar. Pretty heady stuff, right? :]

Seriously, in spite of a lack of fireworks, the important thing is that your new code is now more memory-efficient. While this might seem like overkill for a single button, in practice you’ll typically have a lot more items in your app. Adding items this way from the start will save you from potential headaches down the road.

One more thing: You probably noticed that the Touch Bar automatically positioned your item on the left-hand side. In the next section, you’ll see how to position Touch Bar items in a better way.

Grouping items

It’s time to add three more items to the Touch Bar: “Delete,” “Next Entry” and “Previous Entry.” Because all three of these items relate to the currently selected entry, you’ll put all of them in a single group item instead of adding them individually.

First, add the following property to the NSTouchBarItem.Identifier extension:

static let entryOptions =
  NSTouchBarItem.Identifier(
  "com.raywenderlich.journalyst.entryOptions")

You’ll use this identifier for the group item. Add it in makeTouchBar by changing the array of item identifiers to this:

bar.defaultItemIdentifiers = [.newEntry, .entryOptions]

Now that the Touch Bar is instructed to display the item, you can create it in touchBar(_:makeItemForIdentifier:). Start by adding a new case inside the switch statement, right before the default case, and inside this case, create an item for each of the three actions you’ll add:

case .entryOptions:
  let next = NSButtonTouchBarItem(
    identifier: .init(identifier.rawValue + ".next"),
    title: "Next Entry",
    target: self,
    action: #selector(goToNext))
  let previous = NSButtonTouchBarItem(
    identifier: .init(identifier.rawValue + ".previous"),
    title: "Previous Entry",
    target: self,
    action: #selector(goToPrevious))
  let delete = NSButtonTouchBarItem(
    identifier: .init(identifier.rawValue + ".delete"),
    title: "Delete",
    target: self,
    action: #selector(removeEntry))

In this code, you’ve created these three new items in the same way that you added the “New Entry” button item earlier. Note that the starter project already includes methods for each of these items to call.

Next, you’ll create a spacer item and place it between “Previous Item” and “Delete.” This is a nice touch that helps ensure that your users don’t tap on “Delete” accidentally.

To do this, add the following line to the case after the delete item:

let spacer = NSTouchBarItem(identifier: .fixedSpaceLarge)

Spacer items are built-in Touch Bar items that are created by assigning one of two predefined identifiers to the item: .fixedSpaceSmall or .fixedSpaceLarge.

While all other item identifiers must be unique, you can use as many spacer items with the same identifier as you like.

Finally, create a group item and return it by adding the following code to the end of the case:

let group = NSGroupTouchBarItem(
  identifier: identifier,
  items: [spacer, next, previous, spacer, delete])
return group

Build and run the project, and you should see your new items, including a small space between “Previous” and “Delete.”

Since a group contains these items, the Touch Bar treats them all as a single composite item. They will always be shown and positioned together.

While using a MacBook with a Touch Bar, you might have noticed that some items are centered in the Touch Bar. You’ll add one final touch to your group by centering it.

Each NSTouchBar can define one centered item, and it’s called the principal item. The good news is that designating an item as principal is very easy. Modify your code now by adding the following line to makeTouchBar, just before its return:

bar.principalItemIdentifier = .entryOptions

Build and run your project again. You’ll now see the group item centered inside the Touch Bar.

By the way, the reason you work with identifiers rather than actual items is that whether items are displayed isn’t always up to you. In fact, you can do something here that many developers dread: Give control of this decision directly to your users. In the next section, you’ll see how to let users add and remove items from the Touch Bar.

Customizing the Touch Bar

If there’s one thing nerds like us enjoy, it’s customization options. Apple clearly had this in mind when they created the Touch Bar, as they added app-specific Touch Bar customization. As a developer, it’s relatively easy to add support for this.

Apps that support Touch Bar customization have an additional option called Customize Touch Bar… inside the View menu of the menu bar. To add this option, head over to AppDelegate.swift and add the following line at the start of application(_:didFinishLaunchingWithOptions:):

#if targetEnvironment(macCatalyst)
NSTouchBar.isAutomaticCustomizeTouchBarMenuItemEnabled = true
#endif

Take a moment to savor that supersized property name and read it out loud: “Is automatic customize Touch Bar menu item enabled.” It’s quite the mouthful, but the benefit of this verbosity is that the property name certainly does explain itself. :]

In spite of its length, enabling this property isn’t enough to enable user Touch Bar customization. You’ll need to do three more things:

  1. Add a customization identifier to the NSTouchBar instance.
  2. Enable customization for each item you want to be customizable.
  3. Add a customization label to each of those items.

Head back to RootSplitViewController.swift to do this.

Fist, add an extension at the top of the file, inside the #if macro:

extension NSTouchBar.CustomizationIdentifier {
  static let journalyst = NSTouchBar.CustomizationIdentifier(
    "com.raywenderlich.journalyst.main")
}

Just like the item identifiers, the customization identifier has to be unique for each NSTouchBar instance.

Now, set this identifier and make your two items customizable by adding the following two lines to makeTouchBar, right before you return the bar:

bar.customizationIdentifier = .journalyst
bar.customizationAllowedItemIdentifiers = [.newEntry, .entryOptions]

That takes care of steps 1 and 2.

Next, you’ll deal with step 3 by adding a customization identifier to each item you created. In touchBar(_:makeItemForIdentifier), add the following line before return button:

button.customizationLabel = "Add a new entry"

Finally, do the same for the other case by adding this line before return group:

group.customizationLabel = "Entry Options"

These labels show up on the customization screen. If you don’t set them, you’ll see an ugly warning instead of the labels.

Build and run the project, then select View ▸ Customize Touch Bar…

You’ll see a screen where you can drag and drop each of your items to and from the Touch Bar. It feels kind of magical to drag an item to something outside the screen. Your settings will be saved, so the Touch Bar will stay the same each time you run the app.

Note: If you’re using the Touch Bar simulator, you won’t drag and drop directly to the floating window. Imagine you have a Touch Bar below your screen — you’ll drag to the bottom of the screen as if the Touch Bar was down there.

Congratulations, the Touch Bar was the final bar in your training journey. You’re now a certified “bar”ista — feel free to add that to your resume! :]

This also completes Section 2 of this book. By now, your app should look like a native macOS citizen, while also running on iOS devices. How cool is that?

Key points

  • The Touch Bar is made of NSTouchBar instances.
  • The Touch Bar uses the responder chain to determine which items to show.
  • Each view and view controller can add items to the Touch Bar by overriding makeTouchBar and returning an NSTouchBar.
  • Use templateItems only for lightweight items.
  • For other items, implement NSTouchBarDelegate.
  • Allow customization by enabling the customization menu item, making items customizable, and adding customization labels to the items.

Where to go from here?

To see some other Touch Bar items in action, check out the NSTouchBar tutorial written by Andy Pereira, one of the authors of this book: bit.ly/2kxqPjs.

Each Touch Bar item can be further customized by adding images and changing the fonts or colors of the item. You can read about how to do this here: apple.co/2k8Dx8c.

If you consider yourself a pro-level barista, keep in mind that items can be custom views and even include gesture recognizers. Try to think outside the box and make the Touch Bar an essential part of the way users interact with your app.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC