Chapters

Hide chapters

macOS Apprentice

Third Edition · macOS 26 · Swift 6 · Xcode 26

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("Boss", systemImage: "person.fill") {
      // 4
      // action goes here
    }
    // 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. Views in a toolbar can show text, icon or both. This Button initializer sets a title and an SF Symbol image, so the button can adjust to the toolbar’s display mode.
  4. You’ll add the button’s action — what it does when the user clicks it — later.

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) {

Resume the preview to see the difference:

Toolbar placement
Toolbar placement

The options you’ll use most often are navigation, primaryAction and principal. Use the preview to 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 that 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:

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 always work in any other app.

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, icons 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_toolbar") {

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. This works even without toolbar customization.

Note: There is a bug in SwiftUI that crashes the app if you have more than one window open with a customizable toolbar. The id for the default sidebar toggle is duplicated, which isn’t allowed. This isn’t a problem for this app because you’re about to restrict it to a single window, but if your app requires multiple windows, then do not make the toolbar customizable.

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 repurpose 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", systemImage: "plus") {
    // 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 and an appropriate SF Symbol to match the new look of macOS menu items, which can show both text and icons.
  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 overwrite 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 repurposed 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", systemImage: "person.fill", isOn: $appState.bossMode)
    // 3
    .keyboardShortcut("b")

  // 4
  Button("Different Word", systemImage: "arrow.clockwise") {
    // 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. Again, this includes a title and an SF Symbol.
  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 with title, system image 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 to the Different Word button, 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. Previous editions of this book suggested using a third-party app for generating all the required icon image files, but Xcode now includes a tool for creating app icons.

Right-click the Xcode icon in your Dock and select Open Developer Tool ▸ Icon Composer:

Opening Icon Composer.
Opening Icon Composer.

Once the app opens, click New Document in the file dialog or choose New from the File menu. If this is the first time you’ve opened the app, click Agree and then it will open a new document. You’ll see a window with a default gradient icon in the center and sidebars on the left and right. Since this is for a Mac app, the first thing is to turn off the iOS options. Click the document icon in the top right of the right sidebar to switch to the options. Select macOS Only from the iOS, macOS popup menu and turn off watchOS:

Setting the icon options.
Setting the icon options.

Click the paintbrush icon in the right sidebar to switch to the drawing tools. The first task is to set a gradient for the background. Click the default icon to show the Fill section, with two color wells. The top one is set to Auto, meaning that when you select a color for the bottom one, the top one adjusts automatically to create an appropriate gradient. We want the background to look like paper, so click the bottom color well and use the color picker to choose mostly white with a hint of yellow — FFFFED in hexadecimal. The top color well changes to match:

Choosing the icon background.
Choosing the icon background.

Next, add one or more images to the foreground. These can be SVG or PNG files. The assets folder for this chapter contains a file called snowman_head.png that you can use. Drag this file on to the icon’s background in Icon Composer:

Adding the icon image.
Adding the icon image.

Now it’s your turn to have fun experimenting with all the options. In the left sidebar, the image is inside a group. Both the group and the image have options you can adjust. The bottom of the main area has three buttons so you can preview your icon in the various view modes.

Each setting group in the right sidebar has a popup to allow you to select All or the current preview mode, so use the mode buttons at the bottom to switch so you can customize settings for each mode.

Turn off Translucency for the group in all modes, and in Mono mode, change the group’s Blend Mode to Plus Darker:

Icon group settings
Icon group settings

When you’re happy with your icon, choose File ▸ Save. In the save dialog, set the file name to AppIcon.icon and save it in the same folder as your SnowmanApp.swift file.

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 Remind Me 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’s outside the scope of this book. For all those details, check out macOS Apps Step by Step.

Select Custom and click Next. On the next screen, choose 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 2026-01-23 15-26-04 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. Of course you know there’s nothing malicious in your app, so tell your friends that it’s OK, but that there’s a process to go through before they can run the app for the first time:

  1. After the first warning, open System Settings and go to Security & Privacy.
  2. Scroll down to see a message saying “Snowman.app was blocked to protect your Mac”.
  3. Click Open Anyway.
  4. You’ll see another warning dialog, but this time click Open Anyway and enter your password when asked.

This is only for the first time they run the app — after that, they can run it without any further warnings.

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.

For more information on the new Icon Composer, check out Create icons with Icon Composer from WWDC 2025.

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.