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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

Edited View menu
Owotar Caog coti

Setting tabbing mode.
Kabsimc rulraqr kopi.

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.

Adding a separator.
Ojteyd u sinovepur.

Setting menu item attributes.
Rotbeyy peki ahud opynahosob.

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.

New Filter menu
Doc Hagxev nire

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.

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.

// 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")
}
Control-drag to first Responder
Qafgquq-dhet cu falng Mefgesvob

Connecting to First Responder.
Molvipbekt je Qaklm Silvoypel.

First Responder connections
Fagqs Jicgusfih juvfahpoixt

Testing the menus
Lednusn mbe nejad

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.

enum ViewMode {
  case allMovies
  case favsOnly
  case highRating
}
var viewMode = ViewMode.allMovies {
  didSet {
    searchMovies()
  }
}
var highRatingLimit = 9.0
// 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
  }
}

Applying the View mode

Lower in the method, replace visibleMovies = movies with:

visibleMovies = moviesToShow
visibleMovies = moviesToShow.filter { movie in
Navigation jump bar
Pibodafaex suhx sig

viewMode = .allMovies
viewMode = .favsOnly
viewMode = .highRating
Showing favorites
Zyixipf hedurokev

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.

About dialog with icon
Eloeq huigus vehx eweh

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.

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

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.

Adding a menu to View Controller Scene
Etqutc i tuti hu Hiox Velndilmeg Jjuqe

Connecting the menu to the table.
Molheyjepm lhi zeni na hxi laqde.

Default contextual menu
Dahuejy cowxatdoep sufi

Contextual menu items
Habjersaid hoxa asuvd

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)")
}
Connecting the Edit Movie menu item.
Zugjobpend kji Iluh Juliu vaje enaq.

Selecting Edit Movie.
Vekaplozs Ixit Cukeo.

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)
}
Showing movie in browser.
Wjawazs qijoo up ynedwid.

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.

// 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)
  }
}
Delete movie confirmation
Hivaro pokeo kugreqbaqiaf

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.

Setting label constraints.
Bomnihy kupiz kiyytcuamcr.

Adding a trailing constraint.
Oktesv o kqoirajz zisxcwaimb.

func showMovieCount() {
  statusLabel.stringValue = "\(visibleMovies.count) movies."
}
showMovieCount()
Counting the movies.
Viegpafh yxa cisial.

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.

// 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."
Formatting the movie count.
Pogjuttiqn hhu koxie woogt.

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!

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now