Chapters

Hide chapters

macOS Apprentice

First Edition · macOS 13 · Swift 5.7 · Xcode 14.2

Section II: Building With SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

Section III: Building With AppKit

Section 3: 6 chapters
Show chapters Hide chapters

10. Adding Toolbars & Menus
Written by Sarah Reichelt

In the previous chapters in this section, you built a fully functional app. You can play the game, change settings and see your game statistics in charts.

Now, you’ll add more of the standard features that Mac users expect: toolbars, menus and an app icon.

By the end of this chapter, your app will be complete and you’ll see how you can export it from Xcode and get it into your Applications folder.

Toolbars

Open your app in Xcode or use the project from the starter folder in the downloads for this chapter. Run the app and look at the main window. You already have a toolbar:

Initial toolbar
Initial toolbar

This is because your view uses NavigationSplitView. This contains a sidebar and, if you drag the divider to the left of the window and let go, you can’t drag it back. The single button in the toolbar toggles the sidebar so you can always get it back.

You have no control over that toolbar item, but you can add your own items. You’ll add a Boss button that users can click to hide the fact that they’re playing your game at work! ;]

Open ContentView.swift and add this after the frame modifier:

// 1
.toolbar {
  // 2
  ToolbarItem {
    // 3
    Button {
      // action goes here
    } label: {
      // 4
      Label("Boss", systemImage: "person.circle.fill")
    }
    // tooltip goes here
  } 
}

Stepping through this:

  1. Add a toolbar to NavigationSplitView. Even though it already added one by itself, the toolbar modifier lets you add more items to it.
  2. Insert a ToolbarItem into the toolbar. This is a view type designed for this purpose.
  3. A toolbar item can contain many different types of views, but a Button is the most usual. You’ll add the button’s action — what it does when the user clicks it — later.
  4. Views in a toolbar can show text, icon or both. A Label is ideal for this as it’s a view that contains both. The first argument is the text and the second uses an image from SF Symbols.

Run the app again to check out your new toolbar item:

Toolbar item
Toolbar item

It doesn’t do anything yet, but it’s there.

Before making it work, there are a couple of options to set. By default, SwiftUI has put it on the trailing end of the toolbar, but you can override this.

Replace the ToolbarItem line with:

ToolbarItem(placement: .navigation) {

Unfortunately, the preview doesn’t show toolbars, so run the app again to see the difference:

Toolbar placement
Toolbar placement

The options you’ll use most often are navigation, primaryAction and principal. Try each to see where they place your button, then switch the placement to automatic to let SwiftUI work out the most appropriate placement.

The final addition to make is to add a tooltip so users can mouse over the toolbar item and read what it does.

Replace // tooltip goes here with:

.help("Quick, the boss is coming!")

This adds a modifier to the Button and provides a tooltip and an accessibility hint:

Toolbar button tooltip
Toolbar button tooltip

Now, it’s time to add the action.

Coding the Toolbar Button

The Boss button has to hide the game and display a blank window with an appropriate window title instead.

First, you need a property to store whether to show or hide the game.

Open AppState.swift and add this new property:

@Published var bossMode = false

This is a Boolean that’s false by default, and AppState publishes any changes to it.

Back in ContentView.swift, replace // action goes here with:

appState.bossMode.toggle()

This uses a Boolean method that switches true to false and false to true.

You have the property, and you have the code to change it. Next, you need to decide what views to show.

Still in ContentView.swift, delete all the lines between body and frame. Those are the selected lines in this screenshot:

Lines to delete
Lines to delete

Fill that gap with:

// 1
Group {
  // 2
  if appState.bossMode {
    // 3
    Color.white
      // 4
      .navigationTitle("Work")
  } else {
    // 5
    NavigationSplitView {
      SidebarView(appState: appState)
    } detail: {
      GameView(appState: appState)
    }
  }
}

This looks like a lot:

  1. You’re deciding between two different view types, but SwiftUI needs to know in advance what type of view body returns. One way to work around this is to embed the two options in a Group so that body always returns a Group view.

  2. Check if bossMode is true.

  3. If it is, use Color.white. In SwiftUI, Color is a view and it always expands to fill the available space, so this fills the entire window with white.

  4. It would spoil the whole “work” look if the window still had the Snowman title. A navigationTitle overrides the default app name title.

  5. This chunk is the same as before and ensures that ContentView displays the game views if bossMode is false.

Run the app and toggle the toolbar item:

Boss mode
Boss mode

Animating the Change

Now you and your users are safe if the boss wanders past, but the switch from one mode to the other is rather abrupt.

Add this after the frame modifier:

.animation(.easeInOut, value: appState.bossMode)

The animation modifier monitors its value and, when it changes, applies the specified animation type. In this case, you’re using easeInOut, which starts the animation slowly, speeds it up and then slows down near the end.

To see the other options, delete easeInOut and press Escape to list the autocomplete suggestions. Some variations allow you to specify the duration, but this one needs to stay fast to fool the boss.

Run the app again and toggle to see a smoother change. There’s a slight stutter on the navigationTitle, but the main view changes more gently.

Now you have a functional toolbar with one automatic item and one that you added yourself.

Menus

Every standard Mac app uses the system menu bar at the top of the screen. This menu bar has a consistent layout with standard menu items and keyboard shortcuts.

You’ll often find app controls duplicated in the menus as a way of providing keyboard-based operations and improving discoverability. Menu items display their shortcuts and the Help menu allows you to search the menu items.

You’ve already added two new menu items by creating new scenes. These both have keyboard shortcuts — one is standard and the other is your custom shortcut.

Now, you’ll add more menu items and a completely new menu.

Including Preset Menu Groups

To start with, you’ll tell SwiftUI to include some preset menu groups in your app. Open SnowmanApp.swift and add this after the end of WindowGroup:

// 1
.commands {
  // 2
  SidebarCommands()
  // 3
  ToolbarCommands()
}

Stepping through this:

  1. The commands modifier adds commands to your scene. For macOS apps, this means adding to the system menu. The contents of this modifier are what commands adds to the menu bar.
  2. The default SwiftUI app includes a basic set of menus and menu items, but not a complete set, since they aren’t all needed for every app. There’s a collection of pre-built menu groups that provide specific functionality in the standard formats. SidebarCommands is one of these, and it adds menu controls for the sidebar.
  3. ToolbarCommands is a similar pre-configured set that allows users to control the toolbar.

Run the app now and check out the View menu:

Expanded View menu
Expanded View menu

The tabs items and the full screen option were there before, but you now have toolbar and sidebar commands, complete with keyboard shortcuts.

Note: These presets include standard keyboard shortcuts, like Command-Option-T for toggling the toolbar, but sometimes developers use these standards for different purposes. This doesn’t matter if it’s confined to their own app, but can cause confusion if they create global shortcuts that work from anywhere. Trello is an example of this, as it sets Command-Option-T as the default global shortcut to toggle the app from anywhere. This means that the toolbar shortcut won’t work in any other app, like Finder or Snowman.

The sidebar menu item may seem unnecessary since you already have a toolbar button for that, but it’s the only way to control the sidebar if the user hides the toolbar, and it supplies the keyboard shortcut.

Test these new menu items using the menu bar and the keyboard shortcuts. Notice how the text of the menu items changes to suit your choices.

SwiftUI doesn’t let you say where you want these items to appear — it places them in the conventional locations with the expected titles and shortcuts.

There are more of these preset groups. Most of them deal with text editing and formatting. To see what’s available, search Xcode’s Developer Documentation for Menus and commands, then scroll down to Getting built-in command groups. Always use these if possible instead of creating your own menus, as they provide your users with the most consistent and familiar interface.

Customizing the Toolbar

The menu bar has disabled the Customize Toolbar… item because you haven’t set up the toolbar for customization yet. To make it editable, the toolbar, and each item in it, must have an id.

Open ContentView.swift and replace the ToolbarItem(placement: .automatic) { line with:

ToolbarItem(id: "boss_mode_toolbar_item", placement: .automatic) {

This assigns a String id to the Boss Mode toolbar button.

Next, replace the toolbar { line with:

.toolbar(id: "content_view_tooolbar") {

And now, you’ve set id properties for both the toolbar and the item you added.

Run the app and check out the View menu again. The menu bar has enabled Customize Toolbar… and you can use it to add or remove the Boss Mode button and change the Show settings. You can’t remove the Toggle Sidebar button:

Customizing the toolbar.
Customizing the toolbar.

You can also right-click in the toolbar to set how you want the toolbar items to appear.

Note: There is a bug in SwiftUI’s toolbar code right now that stops the text version of the toolbar items from performing their actions. If you choose the Text Only view option, you won’t be able to use either toolbar item and if you select Icon and Text, only the icon is clickable. Hopefully Apple will have fixed this bug by the time you’re reading this.

Adding a Custom Menu Item

The pre-built menu groups are useful and cover a lot of options, but sometimes, your app needs more. In this app, there’s an item in the File menu to create a new window. Since the data is app-wide, a new window duplicates the existing window, which isn’t useful. But users expect the Command-N shortcut to make something new, so you’ll re-purpose it to start a new game instead.

In SnowmanApp.swift, add a blank line after ToolbarCommands() and insert:

// 1
CommandGroup(replacing: .newItem) {
  // 2
  Button("New Game") {
    // 3
    appState.startNewGame()
  }
  // 4
  .keyboardShortcut("n")
}

Some of this is new to you:

  1. A CommandGroup adds its content to an existing menu. You tell a CommandGroup where to put its content and it can be before, after or replacing a standard menu item. To see a list of the known menu items, delete newItem and press Escape with the cursor after the period. In this case, you’re replacing New Window, so reselect newItem when you’ve read the options.
  2. A CommandGroup can contain one or more menu items. A menu item can be one of a number of SwiftUI views, but it’s most often a Button. In this case, you add a Button with the title New Game.
  3. The button action calls appState’s startNewGame(), which is what the New Game button in the game view does.
  4. Set a keyboard shortcut of Command-N, taking over the previous New Window shortcut while keeping the general usage consistent. Remember that the default shortcut modifier key is Command, so you don’t have to specify this.

Run the app now and open the File menu. Select New Game or press Command-N to see a new game appear in the sidebar:

New Game
New Game

You’ve created a new menu item, positioned it to over-write an existing menu item that you didn’t want and given it the expected keyboard shortcut. That’s good user interface design work!

Hiding a Standard Menu Item

In the previous section, you re-purposed an existing menu item to suit your app. But what if you want to delete an item instead of replacing it?

Right now, if you select Snowman Help from the Help menu, you get no help at all:

Snowman Help
Snowman Help

Maybe you’ll add some help information later, but for now, you’ll hide that menu item.

Type a blank line after the end of the new CommandGroup and add this:

// 1
CommandGroup(replacing: .help) {
  // 2
  EmptyView()
}

Looking at this code, it:

  1. Adds another CommandGroup. This one replaces the help menu item.
  2. One of the views you can put into a CommandGroup is EmptyView(), which shows nothing.

And now, the Help menu only contains the menu search:

Truncated Help menu
Truncated Help menu

Deleting the Help menu item may not provide a great user experience, but knowing how to delete an unwanted menu item is useful.

Inserting a Custom Menu

Sometimes, you need to add a completely new menu. You’ll add a Game menu that has a couple of items to help the user control the game.

To do this, you’ll add a new container to the commands modifier in SnowmanApp.swift.

Add a blank line after the last CommandGroup, and then insert:

// 1
CommandMenu("Game") {
  // 2
  Toggle("Boss Mode", isOn: $appState.bossMode)
    // 3
    .keyboardShortcut("b")

  // 4
  Button("Different Word") {
    // change word action
  }
  .keyboardShortcut("d")
}

What does this do:

  1. A CommandMenu inserts a new menu into the main menu bar. The argument is the menu title, and the contents dictate the menu items.
  2. A Toggle is a great view to use as a menu item if it’s linked to a Boolean value. It looks different from the Toggle you used in SettingsView — in a menu, it gets a checkmark before the title whenever the connected Boolean value is true. In this case, it’s bound to appState.bossMode so selecting this toggles the value. And because its a two-way binding, changing the value using the toolbar button adds or removes the checkmark in the menu item.
  3. It has a keyboard shortcut because you may need to engage Boss Mode in a hurry! The toolbar item could have a shortcut, but it’s not discoverable. The menu item makes it quite clear. And the menu lets you toggle Boss Mode even with the toolbar hidden.
  4. Next, add another Button and shortcut to allow the user to choose a new word. You’ll write the code for that soon.

Run the app now to see your new menu:

Game menu
Game menu

The Different Word item doesn’t do anything yet, but Boss Mode works and its checkmark appears and disappears automatically.

Disabling a Menu Item

Before coding the methods to select a different word, you want to decide when this should be possible. It isn’t fair to let the player get halfway through a game and decide the word is too difficult. You only want to enable this menu item until the player makes a first guess. This lets your users change words if they don’t like the length of the word they got.

To make this easier to use, you’ll create a computed property in AppState. Open AppState.swift and add:

// 1
var gameHasStarted: Bool {
  // 2
  !games[gameIndex].guesses.isEmpty
}

This packs a lot in:

  1. The computed property is a Boolean — either true or false.
  2. Since it’s all on one line, the return keyword isn’t required. This line gets the current game and checks to see if its guesses array is empty. The leading ! reverses the result, so if guesses.isEmpty is true, this returns false and if guesses.isEmpty is false, it returns true.

To use this, go back to SnowmanApp.swift and add this after .keyboardShortcut("d"):

.disabled(appState.gameHasStarted)

This disables the menu item if the game has started, which is true if the player has made any guesses. Using a computed property for this makes it much more readable at the usage point, even if creating the property was confusing.

Run the app and check the menu item. Then enter a single guess and check it again:

Disabled menu item
Disabled menu item

Since all completed games must have at least one guess, this only enables the menu item for games that are in progress, but not started.

And with this in place, you’re ready to code changing words.

Choosing a Different Word

Implementing changing words requires changes to several files. First, open Game.swift and add this method:

// 1
mutating func chooseNewWord() {
  // 2
  word = getRandomWord()
}

What’s happening here?

  1. Because this method changes a value in a structure, you must mark it as mutating.
  2. Then, use getRandomWord() to assign a new word to this game.

The next step is to add a method to AppState to call this, so open AppState.swift and add this:

// 1
func getDifferentWord() {
  // 2
  games[gameIndex].chooseNewWord()
}

Stepping through this:

  1. AppState is a class, so editing a property doesn’t require the mutating keyword.
  2. Find the current game in the games array and call chooseNewWord() on it.

The final step is to hook this up to the menu item. Back in SnowmanApp.swift, replace // change word action with:

appState.getDifferentWord()

This calls the AppState method, and that calls the Game method. You could have combined all this into the button action, but that would not have been so clear. This way, the button talks to AppState. AppState knows which is the current Game, and Game has the method for finding a random word so it can change its own word. Each part takes responsibility for its own properties, and any changes are easy to trace.

Run the app and, before making any guesses, select Game ▸ Different Word or press Command-D. Look in the Xcode console to confirm that the game has chosen a new word:

Choosing different words.
Choosing different words.

You’ve added preset menu groups, menu items and complete menus. You’ve used different view types in the menu items, and everything has a keyboard shortcut. Great work! Mac users will appreciate your efforts.

Adding an App Icon

Take a look at the app icon in the Dock. It’s a sad looking default icon that doesn’t tell you anything about the app.

You may have heard that Xcode no longer needs a huge range of icon image files and can work with a single image. Unfortunately, this is only true for iOS apps. macOS apps still require a set of icon images at different sizes.

Although the largest icon size is 1024 x 1024 pixels, modern Mac icons have rounded corners and padding on all sides. You have to supply the icon like this — if you use a square image, the icon is square too.

Bakery is a great tool for creating the icon files and you can download it for free from the Mac App Store. You can use symbols and colors to make an icon for use during testing, but Apple owns the copyright on these symbols. For release, you need to use your own images.

Download and open Bakery. Next, open the assets folder in the downloaded materials for this chapter, and find the snowman.png file.

Back in Xcode, open Assets.xcassets and delete the existing AppIcon by selecting it in the sidebar and pressing Delete.

With Xcode still visible, return to Bakery and drag snowman.png onto the icon at the top of the window:

Drag image into Bakery.
Drag image into Bakery.

Next, click Generate icons: Bakery makes a floating palette at the bottom left of your screen. Drag this palette into Xcode’s Assets.xcassets to add a new AppIcon:

Importing the icon.
Importing the icon.

The icon has all the image files for both macOS and iOS. With AppIcon selected, press Command-Option-4 to show the Attributes inspector.

Set iOS to None to delete all the excess image files:

Remove iOS icon files.
Remove iOS icon files.

To see your new icon in operation, choose Product ▸ Clean Build Folder and then run the app. Whenever you change a non-code file, cleaning the build folder makes sure that Xcode implements the changes.

Note: Xcode may show a dialog asking you to confirm that you really want to clean the build folder. If you see this, check Don’t ask again and click Clean.

As well as seeing your icon in the Dock, you can choose About Snowman from the Snowman menu and see it in the About box:

About box with icon
About box with icon

And now you’ve added the extras that Mac users expect and appreciate in all native Mac apps.

Exporting your Mac app

So far, you’ve always run your app from Xcode. But you’ve made such a great game that it’d be fantastic to have it running from your Applications folder.

Quit the app if it’s still running and then in Xcode, choose Product ▸ Archive. If you get a message about Xcode Cloud, click Later. Xcode runs the app in Debug mode, which adds extra diagnostic tools, uses more memory and is less efficient. Archiving builds the app in Release mode.

When it’s finished, it opens the Organizer window. You can get back to this window at any time by selecting Window ▸ Organizer:

Archived app in Organizer window.
Archived app in Organizer window.

Select the archive and click Distribute App. This is the start of the process you’d go through for full app distribution, but that is outside the scope of this book. For all those details, check out macOS by Tutorials.

Select Copy App and click Next, then choose a save location for the export folder and click Export. You don’t get any feedback from Xcode at this point, but switch to Finder and locate the new folder. The export process calls it something like Snowman 2022-11-28 19-23-01 depending on when you create the export. Inside that folder is your app, and you can drag it into your Applications folder and run it.

But you won’t be able to see the word in the console anymore, so no more cheating. ;]

macOS happily runs the app on your computer, but complains if you give it to someone else. They’ll see a message saying that macOS can’t open the app because Apple has not checked it for malicious software:

App warning dialog
App warning dialog

Of course you know there’s nothing malicious in your app, so tell your friends to right-click the app and select Open. They’ll see another warning dialog, but if they click Open again, they’ll be able to run your app, and they only have to go through that once.

So there it is. You’ve written a real native macOS app using SwiftUI and you’ve archived it so you can run it and give it to your friends. You’re definitely a Mac app developer now!

Challenge

Your boss may not be convinced by a totally blank white window with no actual work showing. Take a screenshot of a real work window and use that instead. This requires several steps:

  1. Get a suitable image and name it work.png. There’s one in the assets folder if you prefer to use that.
  2. Import the image file into Assets.xcassets.
  3. Replace Color.white in ContentView.swift with an Image using the work file.
  4. Add modifiers to make the image fill the window. Check out GameView.swift to see what modifiers you added to the Image there. Are some of them appropriate? Maybe with different settings?

Try to work this out for yourself, but if you get stuck, look in the challenge folder for this chapter.

Key Points

  • Mac users expect standard apps to operate in similar ways. This includes adding toolbars to main windows and using the menu bar.
  • SwiftUI has options for including preset menu groups. Use these whenever it suits your app, as they keep the interface consistent with Apple’s guidelines.
  • Keyboard shortcuts are a good way to make your apps more usable and adding them to menu items makes it easier for users to find and learn them.
  • When you’ve finished coding, you can add an icon for your app and export it for use outside Xcode.

Where to Go From Here

You’ve done a terrific job and created a great little app. What could you do with it now?

One idea would be to add a different word list. You could make a version for children or one to teach a different language.

Then there are the graphics. There are many variants of this game with different images as the player guesses. Search for hangman game variations to get some ideas, or create your own image sets. Game.swift expects the last image to be number 7, so you’ll need to edit that if you have a different number.

In the next section of this book, you’ll leave SwiftUI and dive into AppKit which is the other layout framework available for macOS apps.

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.