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

13. Powering Up Your Table
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 chapter, you designed and built the interface for users to select a movie and read its details.

Now, you’ll move on to adding more features to the table itself. You’ll create a toolbar with a search field and you’ll make the table columns sortable.

You’ve already given users the ability to mark their favorite movies, but this data doesn’t persist between app launches. In order to store the favorites settings, you’ll learn how to save the data and reload it. This involves interacting with the Mac sandbox.

Adding a Search Field

With over 28,000 movies in the table, scrolling to find the one you want isn’t easy. So it’s time to add search. The appropriate place for a search field is in the window’s toolbar, and that’s where you’ll add it.

Open Xcode with your project from the previous chapter or use the starter project from the downloads for this chapter.

Open Main.storyboard to set up the toolbar. You might expect to add it to ViewController since that’s where you’ll use it, but a toolbar is part of a window, so you add it to the window.

Scroll the display so you can see the window with the Movies title. Press Shift-Command-L to open the Library and search for toolbar, then drag a Toolbar into the window:

The default toolbar.
The default toolbar.

By default, the toolbar has three items (apart from spacers) and you don’t want any of them. Double-click anywhere in the toolbar to open its editor:

The toolbar editor
The toolbar editor

One at a time, select Colors, Fonts and Print and press Delete. Next, open the library again and search for search. :]

Drag a Search Toolbar Item into the toolbar and then drag it from the toolbar, down into the Default Toolbar Items box:

Adding the search item.
Adding the search item.

It ends up appearing twice, once in the allowed items box and once in the default items box. Click Done.

This adds the user interface, but it won’t do anything until you write the code to detect any search input and filter the movies.

Processing Search Input

The search field is in the window, but you want its data in the view controller. You’ll use the window controller to detect changes and pass them on.

var searchText = ""
// 1
import AppKit

// 2
extension NSWindowController: NSSearchFieldDelegate {
}
controlTextDidChange autocomplete
wimxpiqFobbGoqCpapga eawucoyvwiha

// 1
guard
  // 2
  let searchField = obj.object as? NSSearchField,
  // 3
  let viewController = NSApp.keyWindow?.contentViewController as? ViewController
else {
  return
}

// 4
viewController.searchText = searchField.stringValue
Setting the search field delegate.
Jimlijl vti leirbl fuizm sabixuyo.

Responding to a Search

You’ve made the connections. Next, you need to respond to the user’s search input.

var searchText = "" {
  didSet {
    print(searchText)
  }
}
Search text
Ciijcq nitx

var visibleMovies: [Movie] = []
visibleMovies = movies
func searchMovies() {
  // 1
  if searchText.isEmpty {
    visibleMovies = movies
  } else {
    // 2
    visibleMovies = movies.filter { movie in
      // 3
      movie.title.localizedCaseInsensitiveContains(searchText)
    }
  }
  // 4
  moviesTableView.reloadData()
}
searchMovies()

Displaying Search Results

Here, you need to change every reference to movies to visibleMovies, but before you start typing, there’s an easier way. :]

Edit All in Scope
Uvec Icm oq Rjoku

Renaming movies
Duhutafc yuhean

Searching the movies.
Luurhreqd wha guvaik.

Sorting the Table

The movies table shows three column headers, and Mac users expect to be able to sort the table by clicking these. You don’t want to disappoint anyone, so that’s what you’ll add next.

func addSortDescriptors() {
  // 1
  let titleSortDesc = NSSortDescriptor(
    key: "title",
    ascending: true,
    // 2
    selector: #selector(
      NSString.localizedCaseInsensitiveCompare(_:)))
  // 3
  let yearSortDesc = NSSortDescriptor(key: "year", ascending: true)
  let ratingSortDesc = NSSortDescriptor(key: "rating", ascending: true)

  // set sort for each column
}
// 1
let titleColumnID = NSUserInterfaceItemIdentifier("TitleColumn")
// 2
let titleColumnIndex = moviesTableView.column(withIdentifier: titleColumnID)
if titleColumnIndex > -1 {
  // 3
  moviesTableView.tableColumns[titleColumnIndex]
    // 4
    .sortDescriptorPrototype = titleSortDesc
}

// 5
let yearColumnID = NSUserInterfaceItemIdentifier("YearColumn")
let yearColumnIndex = moviesTableView.column(withIdentifier: yearColumnID)
if yearColumnIndex > -1 {
  moviesTableView.tableColumns[yearColumnIndex]
    .sortDescriptorPrototype = yearSortDesc
}

let ratingColumnID = NSUserInterfaceItemIdentifier("RatingColumn")
let ratingColumnIndex = moviesTableView.column(withIdentifier: ratingColumnID)
if ratingColumnIndex > -1 {
  moviesTableView.tableColumns[ratingColumnIndex]
    .sortDescriptorPrototype = ratingSortDesc
}
addSortDescriptors()
Sort headers
Jazv deuponb

Applying the Sort

The NSTableViewDataSource receives a message when the user changes the sort. Open TableData.swift and make space at the end of the extension to add a new method.

sort autocomplete
suwl auyoboygnace

// 1
if let sortedMovies = (visibleMovies as NSArray)
  // 2
  .sortedArray(using: moviesTableView.sortDescriptors) as? [Movie] {
  // 3
  visibleMovies = sortedMovies
  // 4
  moviesTableView.reloadData()
}

Adding Attributes

Whenever you want to be able to refer to a Swift class or property from an Objective-C method, you give it a special attribute. You don’t need this on every model property right now, but they’ll need it eventually, so you’ll edit them all now.

@objc class Movie: NSObject, Codable {
Multi-cursor editing
Hetto-ziqjuv ocixezd

@objc class Principal: NSObject, Codable {
Sorting table columns.
Yibboql pifce kekahvb.

Sorting the Search Results

Open ViewController.swift and find searchMovies. Before reloading the table, insert this:

if let sortedMovies = (visibleMovies as NSArray)
  .sortedArray(using: moviesTableView.sortDescriptors) as? [Movie] {
  visibleMovies = sortedMovies
}
Sorted search results.
Salcuc caapjh bifiygp.

Saving the Table Setup

Run the app and change the way you display the table. Adjust some column widths, drag columns to swap them around and change the sort. Resize and move the window.

Setting table autosave.
Rayyobh zenhe uibojowi.

Column setup restored.
Yijayw zexoy temropip.

Saving Your Data

Every time the app starts, it reads in the list of movies from movies.json. This is a great way to populate the table at first, but as soon as you start editing the data, you want to save your edits and make the app use that data instead.

struct DataStore {
}
var dataStore = DataStore()
movies = dataStore.readBundleData()

The Mac Sandbox

When you create an app project, it doesn’t get access to everything on your Mac. It has its own sandbox where it’s allowed to read and write files. Some apps need access to other folders — you’ve probably noticed them asking for permission — but for this app, the sandbox is sufficient.

Container folder info
Cumluotit rutkak ijvu

Container folder contents
Gighaekin hazhav hifcibky

Saving Movie Data

Open DataStore.swift and add this computed property:

var savedDataURL: URL {
  URL.documentsDirectory.appending(component: "movies.json")
}
func saveData(movies: [Movie]) {
  // 1
  do {
    // 2
    let jsonData = try JSONEncoder().encode(movies)
    // 3
    try jsonData.write(to: savedDataURL)
  } catch {
    // 4
    print(error)
  }
}
dataStore.saveData(movies: movies)
Saved data file in container
Devop gija wiba ul qawbeicej

Reading Saved Data

At the moment, you’re calling readBundleData() in DataStore to read the default data file. You still want to use this method, but only if there’s no stored data file.

// 1
func readStoredData() -> [Movie] {
  // 2
  do {
    let jsonData = try Data(contentsOf: savedDataURL)
    let movies = try JSONDecoder().decode([Movie].self, from: jsonData)
    let sortedMovies = movies.sorted(using: KeyPathComparator(\.title))
    return sortedMovies
  } catch {
    // 3
    return readBundleData()
  }
}
movies = dataStore.readStoredData()
Displaying stored data.
Jexrdopoym wribon huta.

Working with Threads

You may have noticed that when you click the heart to toggle a movie’s favorite status, there’s a slight delay. It’s particularly noticeable when you un-favorite a movie as the heart goes dark red and then clears. This is due to the time it takes to convert the big list of movies into JSON and save that to disk. While your app is busy with that, it hasn’t time to update the interface.

// 1
DispatchQueue.global().async {
  do {
    let jsonData = try JSONEncoder().encode(movies)
    try jsonData.write(to: savedDataURL)
  } catch {
    print(error)
  }
// 2
}
Saving asynchronously.
Guqudf otznsztasualks.

Key Points

  • In an AppKit app, a toolbar is part of the window.
  • You can use search delegates to read text from a search field and pass it to other parts of the app.
  • Tables need sort descriptors to make columns sortable. Sorting based on sort descriptors uses NSArray methods.
  • Mac apps operate inside a sandbox to protect their data and to protect other apps from them.
  • Background threads can be used for tasks that don’t change the display. This keeps your app’s interface responsive.

Where to Go From Here

You’ve done a lot of work in the main window, but so far, you haven’t looked at the main menu bar. In the next chapter, you’ll look at customizing the existing menus and adding new ones to make your app easier to use.

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