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

14. Enhancing Your App
Written by Sarah Reichelt

In the previous chapters, you created a table-based app. Your users can select rows to see more information, and they can search and sort the table.

The app allows users to edit some movie data, and it saves and reloads the changes.

Every Mac app uses the system menu bar at the top of the screen. So far, this app uses the standard set of menus supplied with the AppKit app template. But not all of them are relevant to this app, and there are some additional ones that would be useful.

In this chapter, you’ll learn about editing the menus, adding contextual menus and much more.

Examining the Menu Bar

Open the MovieTables project from the last chapter or use the starter from the downloads for this chapter. Run the app and look at the default menus in the menu bar:

Default menus
Default menus

Working from left to right:

  • The Apple menu is part of macOS and not under app control.
  • The MovieTables menu is standard, so you won’t change it.
  • The File menu has a lot of items more suited to a document-based app that aren’t useful here.
  • The Edit menu has a lot of items dealing with formatted text. This app will add text editing later, but it doesn’t need all this.
  • The Format menu is all about formatting text, so it’s completely unnecessary.
  • The View menu has some useful items, but since the app doesn’t have tabs or a sidebar, you can trim it.
  • The Window menu is fine. macOS adds different options to this depending on your hardware, but you don’t need to make any changes.
  • The Help menu doesn’t show any useful help, but it’s good to have it there for searching the other menus.

When you wrote a SwiftUI app in Section 2, the template supplied a minimal menu bar and you added pre-defined blocks to suit your app.

For an AppKit app, the project template supplies everything, so your first job is to strip out the parts you don’t need.

Open Main.storyboard and select Application Scene in the outline view. This shows the menu bar in the storyboard:

Application Scene
Application Scene

Expand Application Scene ▸ Application ▸ Main Menu ▸ File ▸ Menu in the outline:

Menu structure
Menu structure

The structure takes some deciphering, so select Main Menu and press Command-Option-4 to open the Identity inspector. The Class at the top shows that Main Menu is an NSMenu, which makes sense.

Now select and inspect File. Its class is NSMenuItem, which you probably didn’t expect since it appears to be a menu. But it contains its own Menu and that is an NSMenu. All the objects inside that are NSMenuItems.

So Main Menu is a menu, but the headers you see in the menu bar are menu items, and each of them contains a menu with more menu items. That’s not confusing at all. :]

Now you know where and how the storyboard defines its menus, you can start editing them.

Trimming the Menu Bar

You don’t want the Format menu at all, so select it in the outline view and press Delete. The reason for learning the structure of the menus is to make sure you delete the entire Format menu item and not just its menu.

If you select Format in the storyboard and press Delete, you only trash its menu, and there will be a space between Edit and View in the display. If this happens to you, use the outline view to eradicate Format completely.

In the View menu, delete Show Sidebar. The toolbar and full screen items can stay.

On to the Edit menu where you can get rid of a lot. Delete all the items after Select All, including the dividing line, which is a special type of menu item. Then delete Paste and Match Style.

And back at the File menu, delete everything except Close.

Run the app and have a look at the menus. They’re a lot neater now. While it’s great to supply menus for everything the user needs, it’s also good design to remove anything that isn’t necessary.

Take a look at the View menu. It still shows some tab bar options and so does the Window menu:

Edited View menu
Edited View menu

These menu items don’t appear in the Application Scene, so you can’t get rid of them that way. The solution is to change a window setting so it doesn’t support tabs.

Select the Movies window (in Window Controller Scene ▸ Window Controller) in the storyboard and press Command-Option-5 to open its Attributes inspector. Change Tabbing Mode to Disallowed:

Setting tabbing mode.
Setting tabbing mode.

This gets rid of all tab bar options in the menus. Now you’ve removed all the unwanted items, and you’re ready to add new ones.

Adding a New Menu Item

You’ll add an item to the Edit menu to toggle the Favorite setting for a movie. It’ll have a keyboard shortcut, so users can do this without clicking. It’s often a good plan to replicate functions in the menus. It makes them more discoverable and adds visible keyboard shortcuts.

Open the Edit menu and press Shift-Command-L to open the Library. Search for menu and drag a Menu Item to the top of the menu. Open the Library again, and this time, drag a Separator Menu Item to below your new item:

Adding a separator.
Adding a separator.

Select the new item and press Command-Option-5 to get to the Attributes inspector. Set its Title to Toggle Favorite, then click in the Key Equivalent box and press Command-T to set the keyboard shortcut. If you mistype, click the X button in the box and try again:

Setting menu item attributes.
Setting menu item attributes.

Command-F might seem more logical, but that’s used for Find in most apps, and you don’t want to confuse your users.

You’ve removed and inserted items in existing menus. Now, you’ll create your own menu.

Inserting a New Menu

There are a lot of movies, and sometimes, it’s nice to see a shorter list. Listing only your favorite movies would also be useful. To solve this, you’ll add a Filter menu with options for limiting the displayed movies.

Remember, an entry in the menu bar is actually a menu item. Click the name to close any open menu in the storyboard. Open the Library and drag a Menu Item into the bar between View and Window. It appears in the bar as a blank space.

Use the Attributes inspector to change its title to Filter, but don’t worry that it still only appears as a blank.

Next, use the Library to drag a Menu into this blank space. Give the menu a title of Filter, and the name finally appears:

New Filter menu
New Filter menu

The menu came prefilled with three items. Use the Attributes inspector to edit them as follows:

  1. Title: Show All Movies, Key Equivalent: Command-M
  2. Title: Favorites Only, Key Equivalent: Command-L (L for Love)
  3. Title: Highest Rated Movies, Key Equivalent: Command-R

The UI is in place, so now it’s time to connect these menu items to the code.

The First Responder

In AppKit, every app has a First Responder. This is whatever is active and can receive events at the time. It could be a text field, a button, a view or a window. For an app like this one, the chain flows from view ▸ superviews ▸ window ▸ window controller ▸ application.

Checking the storyboard, each scene includes a First Responder object to accept input and events. The preset menu items all send messages to the First Responder.

To see this, select File ▸ Close in the Application Scene and press Command-Option-7 to open the Connections inspector. The action for this menu item sends performClose to the First Responder. When you choose this menu item, the message travels up the responder chain until it gets to an object that can handle this method. Since performClose is an NSWindow method, the window handles this action.

For your own menu items, you’ll create action methods in ViewController, which is part of the responder chain.

Adding Menu Actions

Previously, to create an action, you Control-dragged from the storyboard into ViewController. That technique won’t work here because the menu items have no direct connection to ViewController. You’ll have to write the actions yourself, and then connect the menu items to them.

Open ViewController.swift and scroll to the bottom. Make some space before the closing curly brace and add this:

// 1
// MARK: - Menu Actions

// 2
@IBAction func showAllMovies(_ sender: Any) {
  // 3
  print("Show all movies")
}

// 4
@IBAction func showFavs(_ sender: Any) {
  print("Show favorites")
}

@IBAction func showHighRated(_ sender: Any) {
  print("Show highest rated")
}

Stepping through this:

  1. A MARK is a special comment that adds a label to the jump bar navigator at the top of the code pane. If it has a dash before the text, it adds a divider line too. This makes navigating around your code easier as your program gets longer.
  2. This is the same format as the action Xcode inserted for favButtonClicked(_:). @IBAction marks this as a method that the storyboard can access. The sender is the object that initiated this call, and it can be any type.
  3. This action will eventually show all movies, but for now, print something to show it works.
  4. Create similar actions for showing favorite movies and highest rated movies.

Back in Main.storyboard, open the Filter menu and Control-drag from Show All Movies to the orange 1 icon in the bar above. This represents the First Responder:

Control-drag to first Responder
Control-drag to first Responder

You’ll see a huge scrolling list of possible actions.

Type show to scroll to the actions starting with show and select showAllMovies::

Connecting to First Responder.
Connecting to First Responder.

Repeat this for the other two Filter menu items, selecting showFavs: and showHighRated:.

When you’ve done that, select the First Responder icon and open its Connections inspector. There are a lot of connections, but they’re in alphabetical order, so scroll down until you find the show… actions. Mouse over them, and the storyboard highlights the connected menu items so you can confirm that you linked the right menu item to the right method:

First Responder connections
First Responder connections

Next, open the Edit menu and select Toggle Favorite. You’re probably starting to type another @IBAction already, but you don’t have to. :] The favButtonClicked action is already there and does what you want.

Control-drag from Toggle Favorite to First Responder and choose favButtonClicked:. You’re duplicating functionality in the menu, but you don’t have to duplicate the code.

Run the app now and test your new menu items:

Testing the menus
Testing the menus

Toggle Favorite works already, and the others print the expected reports, whether you select the item from the menu or use the keyboard shortcut.

You’ve made all the links between the storyboard and ViewController. Next, you’ll fill in the code.

Coding the Menus

You’ve set up menus to switch between three different view modes. An enumeration is always a good choice to manage a set of states like this.

Open the Data & Models group in the Project navigator and make a new Swift File called ViewMode.swift.

Add this enumeration:

enum ViewMode {
  case allMovies
  case favsOnly
  case highRating
}

This sets up a case for each of the three possible modes.

Next, open ViewController.swift and add these properties:

var viewMode = ViewMode.allMovies {
  didSet {
    searchMovies()
  }
}
var highRatingLimit = 9.0

This creates a ViewMode property and sets it to allMovies by default. Any time you set a new value, call searchMovies() to update the list of displayed movies. It also sets up a property to hold the limit for a high rated movie.

You already have a method to search and sort the movies. This is the logical place to add the filter, so scroll to searchMovies(). Add some blank lines at the start of the method and insert this code:

// 1
var moviesToShow = movies
// 2
if viewMode == .favsOnly {
  // 3
  moviesToShow = movies.filter { movie in
    movie.isFav
  }
} else if viewMode == .highRating {
  // 4
  moviesToShow = movies.filter { movie in
    movie.rating >= highRatingLimit
  }
}

What does this do?

  1. Make a copy of the complete movies array. This is the default list for display.
  2. If viewMode is favsOnly, loop through movies, using filter to find the favorites only.
  3. Or if viewMode is highRating, use filter to find the ones with a rating higher than or equal to highRatingLimit.

Now moviesToShow contains the movies matching the view mode, but you aren’t using it yet.

Applying the View mode

Lower in the method, replace visibleMovies = movies with:

visibleMovies = moviesToShow

And replace visibleMovies = movies.filter { movie in with:

visibleMovies = moviesToShow.filter { movie in

This starts any search with the filtered moviesToShow.

You’re nearly at the end of the menu processing now. Use the navigation jump bar to move to showAllMovies:

Navigation jump bar
Navigation jump bar

Replace the print line with:

viewMode = .allMovies

This sets the correct viewMode and the property observer you created earlier calls the search method to process it.

Similarly, replace the print in showFavs(_:) with:

viewMode = .favsOnly

And in showHighRated(_:), use:

viewMode = .highRating

Time to check it out. Run the app, make sure you have a few favorite movies marked and test the menu:

Showing favorites
Showing favorites

Test the Highest Rating mode too. Sort by Rating to make sure you can’t see anything with a rating lower than 9.0.

You’re nearly done with the system menu bar, but there’s just one more thing… :]

The MovieTables Menu

Your app has some standard menu items in the MovieTables menu. One of these is About MovieTables. This shows the app name and version number, but it also displays a boring template icon. Adding a custom icon would improve this dialog and the Dock display.

In the assets folder in the downloads for this chapter, you’ll find an AppIcon.appiconset folder that contains a pre-built icon set. To install this, open Assets.xcassets from the Project navigator. Select the existing AppIcon in the sidebar and press Delete to trash it.

Then drag AppIcon.appiconset from the assets folder into the Assets.xcassets sidebar. Press Shift-Command-K to clean the build folder, then run the app. Select MovieTables ▸ About MovieTables to see your new icon in the About box:

About dialog with icon
About dialog with icon

The MovieTables menu also contains a disabled item for Settings…. One of the constant themes of this book has been to supply the features the Mac users expect, and they definitely expect a Settings window. So why isn’t there one for this app?

It’s coming. In the last section of the book, you’ll learn about mixing SwiftUI and AppKit. You’ll add a Settings view, built using SwiftUI, to this app. So for now, leave the menu item in place even though it doesn’t do anything.

App Delegate

You’ve learned about using delegates for the table and the search bar, but you haven’t looked at AppDelegate yet. It’s a subclass of NSApplicationDelegate, and it receives notifications about application events, as well as providing values for some application properties.

Open AppDelegate.swift to see the three methods supplied in the standard template.

  • applicationDidFinishLaunching(_:) gives you a place to perform any app-wide initialization.
  • applicationWillTerminate(_:) is the opposite and allows you to take actions before the app closes.
  • applicationSupportsSecureRestorableState(_:) specifies whether this app should restore its previous state on launch.

This app doesn’t do any of these things, and since unnecessary code is bad code, delete everything inside the AppDelegate class definition.

But, there’s one thing you need to add. Right now, if you run the app and close the window, you’re stuck. This is a single window app, and there’s no way to get the window back if you close it.

The solution is to add this method to AppDelegate:

func applicationShouldTerminateAfterLastWindowClosed(
  _ sender: NSApplication
) -> Bool {
  true
}

This is a message that the app sends to AppDelegate when the last window closes, and as it returns true, the app then quits. This is good behavior for a single window app.

Contextual Menus

So far, you’ve looked at menus in the system menu bar, but you can attach menus to other objects in your interface. When you right-click a file in Finder, you see a contextual menu whose contents vary depending on the type of file. You’ll add a similar menu to the table.

Start with the visuals. Open Main.storyboard and scroll to see the view with the table. Click the bar above the ViewController to make it active. Press Shift-Command-L to open the Library and search for menu. Drag a menu into the bar:

Adding a menu to View Controller Scene
Adding a menu to View Controller Scene

This adds the menu to the scene, but it’s not connected to the table. Select Movies Table View using the Shift-right-click menu. Press Command-Option-7 to open the Connections inspector.

In the Outlets section, drag from the circle beside menu to the menu you just added to the bar:

Connecting the menu to the table.
Connecting the menu to the table.

Run the app and right-click anywhere in the table to make the menu appear, showing the default three items:

Default contextual menu
Default contextual menu

There are a few things to note here. The menu only appears when you right-click in the table, not anywhere else. The items are inactive because you haven’t connected them to any actions yet.

Most interestingly, the selected line and the line that activated the menu aren’t necessarily the same. Your code has to allow for this.

Back in Main.storyboard, edit the three menu items: Use the Attributes inspector or double-click the text in the menu. Using Option-; to type the three dots as a single character, set the menu items to Edit Movie…, Delete Movie… and Show in Browser.

The first two menu items end in an ellipsis because they’ll lead the user to another window or dialog.

Open the Library again and drag a Separator Menu Item between the second and third items, so your menu looks like:

Contextual menu items
Contextual menu items

With the items in place, your next task is to set up their actions.

Setting a Contextual Action

Open ViewController.swift and scroll to the bottom. Add some blank lines and then insert this:

// 1
// MARK: - Contextual Menu Actions

// 2
func clickedMovie() -> Movie? {
  // 3
  let row = moviesTableView.clickedRow
  // 4
  if row > -1 {
    return visibleMovies[row]
  }
  // 5
  return nil
}

// 6
@IBAction func editMovie(_ sender: Any) {
  // 7
  guard let movie = clickedMovie() else {
    return
  }
  // 8
  print("Editing \(movie.title)")
}

Taking this bit by bit:

  1. Add another MARK to divide up the code.
  2. Create a method to return the right-clicked movie, if possible. All three menu items need to access this, so it’s worth separating into its own method.
  3. Query the table for its clickedRow property. This gives the index of the row the user right-clicked in, which may not be the selected row.
  4. If the user right-clicked outside the table rows, row is -1. Otherwise, use row to find the movie in visibleMovies.
  5. If row is -1, return nil to tell the actions to do nothing.
  6. Add another @IBAction that you can connect to the menu item.
  7. It starts by looking for a valid clickedMovie and returns if there isn’t one.
  8. Editing movies comes later, so for now, print the movie title to show the menu works.

Now to connect the menu item to this action. Open Main.storyboard and select the Edit Movie menu item.

Control-drag from the menu item to the circular icon in the bar that represents the View Controller and choose editMovie: from the menu:

Connecting the Edit Movie menu item.
Connecting the Edit Movie menu item.

Note: This demonstrates a different way to make a connection. Before you used the Connections inspector, or you Control-dragged into the outline view. Try them all and use whichever you find most intuitive.

You’ve only linked one menu item, but that’s enough to test the menu. Run the app, right-click a movie and select Edit Movie to see the expected text appear in the Xcode console:

Selecting Edit Movie.
Selecting Edit Movie.

You’ve coded one menu item and connected the action to the item. Now, it’s time to complete the other two.

Showing a Movie in the Browser

Start by adding the Show in Browser action. Open ViewController.swift, scroll to the end and add this:

// 1
@IBAction func showInBrowser(_ sender: Any) {
  // 2
  guard let movie = clickedMovie() else {
    return
  }

  // 3
  let address = "https://www.imdb.com/title/\(movie.id)/"
  // 4
  guard let url = URL(string: address) else {
    return
  }
  // 5
  NSWorkspace.shared.open(url)
}

What’s happening here?

  1. Set up an @IBAction as before.
  2. Again, check the user has right-clicked a movie.
  3. Each movie has an id property that identifies it in the IMDb database. Assemble a web address for the selected movie by interpolating its id.
  4. Check that this creates a valid URL.
  5. NSWorkspace is a class that can interact with other apps. NSWorkspace.shared gives you the workspace that’s available to the app and open uses the default application for the URL. Since this URL is a web address, it opens it in the user’s default browser.

With the action in place, head back to Main.storyboard to connect it up. Control-drag from Show in Browser to View Controller and select showInBrowser:.

Time to test this one. Run the app, right-click any movie and select Show in Browser:

Showing movie in browser.
Showing movie in browser.

It works! Time to implement the last action.

Deleting a Movie

Deleting a movie is complicated because you shouldn’t do something destructive without confirmation. So, this action needs to show a dialog.

In ViewController.swift, add this method:

// 1
@IBAction func deleteMovie(_ sender: Any) {
  guard let movie = clickedMovie() else {
    return
  }

  // 2
  let alert = NSAlert()
  alert.alertStyle = .warning
  alert.messageText = "Really delete '\(movie.title)'?"

  // 3
  alert.addButton(withTitle: "Delete")
  alert.addButton(withTitle: "Cancel")

  // 4
  let response = alert.runModal()

  // 5
  if response == .alertFirstButtonReturn {
    // 6
    movies.removeAll {
      $0.id == movie.id
    }

    // 7
    clearSelectedMovie()
    searchMovies()
    dataStore.saveData(movies: movies)
  }
}

This is quite a chunk of code:

  1. The first section is familiar to you — create an @IBAction and check if the user right-clicked a movie.
  2. Create an NSAlert, which is the class that displays a dialog box. Set its style — the options are critical, warning or informational — and set the message using the movie’s title.
  3. Add two buttons to the alert. The first one added is always the default selection if the user presses Return.
  4. Run the alert to show it and wait for the user to respond. This returns an NSApplication.ModalResponse.
  5. Since the second button does nothing, you only need to check if the user clicked the first button, which returns alertFirstButtonReturn.
  6. Use an Array method to delete any movies that match the criteria. In this case, id is a unique identifier, so you’ll only remove one.
  7. Clear the detail display, use searchMovies() to update the table, then save the edited data.

Phew, that’s quite a method! Now, you to connect it to its menu item.

I’m sure you’ve guessed what’s coming. Open Main.storyboard and Control-drag from Delete Movie to View Controller. Select deleteMovie: to complete the link.

Ready to test? Run the app and try deleting a movie:

Delete movie confirmation
Delete movie confirmation

Click either button to test. Or, press Escape to click Cancel or Return to click the default Delete.

Note: If you ever want to return to the default list of movies, quit the app and switch to Finder. Open Library ▸ Containers ▸ MovieTables ▸ Data ▸ Documents and delete movies.json. Next time you run the app, it’ll read the default data file.

Great work! Your table now has a contextual menu, and two of the three items are fully functional.

Showing the Movie Count

When the app reads the movies data file, it prints the number of movies. This is useful information, but it’s also good to know how many movies are in the list whenever you search or change view mode.

To add this, open Main.storyboard and scroll to see the blank space underneath the table in View Controller. You left that space there for this exact purpose. :]

Open the Library and drag a Label into this space.

Click Add New Constraints and set top spacing to Standard and left spacing to 10. Click Add 2 Constraints:

Setting label constraints.
Setting label constraints.

Control-drag from the new label into the table area and choose Trailing from the popup menu. This locks the right edge of the label to the right, or trailing, edge of the table box:

Adding a trailing constraint.
Adding a trailing constraint.

You’ve positioned the label, but now it needs a name, so the code can access it.

Option-click ViewController.swift in the Project navigator to open it in a secondary editor. Scroll to the top and Control-drag from the new label to where you defined the other @IBOutlet properties. Insert an Outlet called statusLabel.

After searchMovies(), add this new method:

func showMovieCount() {
  statusLabel.stringValue = "\(visibleMovies.count) movies."
}

This sets the text of the status label to the number of movies displayed in the table, accounting for searches and filters.

In viewDidLoad(), add this line at the end to call your new method:

showMovieCount()

And, since this now displays the count in the app, delete the print line.

Finally, scroll to the end of searchMovies() and add the same line.

Run the app and check the count:

Counting the movies.
Counting the movies.

Do a search and change the view mode to confirm that the status label shows the number of visible movies.

This is good, but large numbers are easier to read if they’re formatted.

Formatting the Count

Adding the thousands separator will make the movie count much more readable. But this isn’t easy to do. Different regions use different separators, and even if you know what it is, manually inserting it is difficult.

Fortunately, Apple has provided a number formatting class for this.

Replace the contents of showMovieCount() with:

// 1
let formatter = NumberFormatter()
// 2
formatter.numberStyle = .decimal
// 3
formatter.locale = Locale.current

// 4
let numberString = formatter.string(for: visibleMovies.count)
  // 5
  ?? "\(visibleMovies.count)"

// 6
statusLabel.stringValue = "\(numberString) movies."

Going through these lines:

  1. Create a number formatter.
  2. Set its style to the one that shows thousands separators. The number of movies is an Int, so even though you’ve chosen the decimal style, it won’t show any numbers after the decimal point.
  3. Set the region for the formatter to the user’s locale. This uses a comma for the thousands separators in the US, a space in France and whatever is appropriate all around the world.
  4. Use the formatter to convert visibleMovies.count into a formatted string.
  5. The conversion can fail, so fall back to converting visibleMovies.count into a string, if necessary.
  6. Display the string in the status label.

Run the app now, and check the display with all the movies visible:

Formatting the movie count.
Formatting the movie count.

That’s a much clearer display.

Key Points

  • The menu bar is an important part of any Mac app. Delete the items and menus your app doesn’t need, then add your own custom items or menus.
  • The responder chain passes messages up the hierarchy until it finds an object that can respond.
  • You can add a menu and connect it to any view to create a contextual menu.
  • A number formatter makes numeric data more readable.

Where to Go From Here

You’ve covered a lot in this chapter. You know how to delete and add menus and menu items to the system menu bar. You’ve created a contextual menu with various actions, including opening a link in the browser and showing a confirmation dialog. And finally, you added a status label with a number formatter. That was a lot of work!

Over the next two chapters, you’ll make a new window for editing movies. You’ll learn a different way to set up a table, and you’ll handle editable text fields.

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.