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

11. Beginning AppKit
Written by Sarah Reichelt

In the previous two sections, you learned some basic Swift and built a complete app using SwiftUI. Apple provides two different layout frameworks for building macOS apps and in this section, you’ll use the other one: AppKit.

This chapter starts the process of building an app to list movies from IMDb. By the time you’re finished, you’ll be able to search, sort, filter and edit your movies list.

You’ll start from scratch with a new style of app template and build your interface graphically using a storyboard. Then, you’ll learn one way to get data into an AppKit table.

When you’re finished, your app will look like this:

Movie Tables app
Movie Tables app

I’m sure you’re excited to proceed, so start Xcode and get ready to code.

Creating an AppKit App

Once you’ve started Xcode, create a new project from the Welcome to Xcode window, or by selecting File ▸ New ▸ Project…. As before, choose macOS ▸ App before clicking Next.

Set the Product Name to MovieTables and leave Team as None or select your own developer team. Enter your reverse domain for the organization identifier — you can make one up, it doesn’t have to be a real domain.

Now for the different setting: Select Storyboard for the interface. Leave language set to Swift and when your options look like this, click Next:

Setting project options.
Setting project options.

Decide where to save your project and click Create.

Note: It always works best to name your Xcode projects with no spaces and no characters apart from letters and numbers. You can change the app name later, but you’ll avoid Xcode issues if you start like this.

Click the Play button in the toolbar or press Command-R to run the app:

Running the AppKit template
Running the AppKit template

There’s a single empty window and if you click the menus, you’ll see a lot of menu items.

Return to Xcode and look at the starting files:

Starting files
Starting files

Your SwiftUI app had Assets.xcassets for images, icons and other graphical assets, and it had an entitlements file to configure the app permissions. The others are new:

  • AppDelegate.swift: A delegate is an object that receives events from, and provides information about its parent. AppDelegate is a subclass of NSApplicationDelegate and it handles interactions with the application itself.
  • ViewController.swift: In SwiftUI, you create Views that the app shows in Scenes. In AppKit, you create subclasses of NSViewController. Each view controller has a view that contains the interface. The view controller acts as the middleman between the view and the data models. This is the Model-View-Controller or MVC pattern and is the default pattern for AppKit apps.
  • Main.storyboard: This is where you’ll design the user interface. You’ll spend a lot of time in this file as you work through this chapter.

What is AppKit?

By this stage, you may be wondering what AppKit is and why you’re using it.

AppKit is the original layout framework created for OS X and, later, macOS apps. You’ll notice that all the main classes have names that start with NS. This stands for NeXTSTEP, which was the company that developed what became OS X. Any time you see a class with the NS prefix, you know you’re using AppKit.

You created SwiftUI views programmatically, but you’ll design your AppKit views visually in a storyboard.

SwiftUI is a great layout framework, but it’s still young. This means that most established macOS apps — and jobs — still use AppKit. As part of your journey to becoming a Mac developer, it’s important to learn both.

The other reason for using AppKit is that SwiftUI doesn’t do everything yet. And some of the things that it does aren’t suited to all apps. This app lists thousands of movies. SwiftUI gets very slow and unresponsive with that much data, so this is an example of where AppKit is better than SwiftUI.

Adding Data

In the SwiftUI app, you built the interface first and then constructed the data model to suit. This time, you’re getting data from an external source so it works best to look at that first and then build the interface around it.

Open the assets folder in the downloaded materials for this chapter and drag movies.json into the Project navigator.

Check Copy items if needed and the MovieTables target. The other options only apply to adding folders, so they don’t matter for this. Then click Finish.

Select the new file to open it in Xcode. It’s a large file — over 36 MB — so Xcode takes a while to open it, but you only need to do this once to examine the structure. The file contains data downloaded from IMDb datasets.

This is a JSON file, which stands for JavaScript Object Notation. JSON has become the standard for transferring object data and Swift handles it well, but first you need to discover the data properties.

After collapsing some entries, this is the first movie in the list:

Structure of the JSON data
Structure of the JSON data

Looking at the top of the file, the opening square bracket indicates an array of objects. The curly braces delimit each object. The data fields and types are:

  • genres: String
  • id: String
  • isFav: Boolean
  • principals: Array
  • rating: Double
  • runTime: Int
  • title: String
  • year: String

principals is another array of objects but you can ignore it for now.

Armed with this knowledge, you can construct your data model.

Creating the Data Model

Press Command-N or use the File menu to make a new file. Choose the macOS ▸ Source ▸ Swift File template and click Next. Name the file Movie.swift and click Create.

Add this class definition to the new file:

// 1
class Movie: Codable {
  // 2
  let id: String
  // 3
  var title: String
  var runTime: Int
  var rating: Double
  var genres: String
  var year: String
  var isFav = false
  // 4
}

This defines the main data structure:

  1. To work with AppKit tables, you must use a class, not a struct. The class conforms to Codable, which means that it works with JSON data.
  2. The id property is a constant so it can never change.
  3. Later on, you’ll let users edit the rest of the movie data, so the other properties are variables.
  4. All the properties from the JSON data are there, except for principals. That needs its own data object, so omit it for now. The names of the class properties match the names of the JSON properties.

If you remember earlier chapters, a structure doesn’t need you to define an initializer, but a class does. However, you don’t need to type it out.

With the cursor in the word Movie, press Shift-Command-A and select Generate Memberwise Initializer:

Generate initializer
Generate initializer

This adds an init method that assigns the arguments to the matching properties.

Note: The internal keyword sets an access level for this method so that it’s only accessible from within this project. This is the default setting for all methods and properties.

Now that you have the object structure, you can decode the JSON data into this format.

Converting the JSON

Before you can display the data in a table, you need a method that reads movies.json and converts the JSON into an array of Movie objects.

Still in Movie.swift, but outside the class, add this:

// 1
extension Movie {
  // 2
  static func readBundleData() -> [Movie] {
    // 3
    guard let fileURL = Bundle.main.url(
      forResource: "movies",
      withExtension: "json")
    else {
      // 4
      return []
    }

    // 5
    do {
      // 6
      let jsonData = try Data(contentsOf: fileURL)
      // 7
      let movies = try JSONDecoder().decode([Movie].self, from: jsonData)
      // 8
      let sortedMovies = movies.sorted(using: KeyPathComparator(\.title))
      return sortedMovies
    } catch {
      // 9
      print(error)
      return []
    }
  }
}

Taking this bit by bit:

  1. Create an extension of the Movie class. This is part of the class, but separating it like this makes it clearer that this is a class method.

  2. Define a static method. You call this method on the class itself, not on any particular instance of Movie. It returns an array of Movie objects.

  3. Start by trying to get the URL, or file address, of movies.json in the application bundle.

  4. If this fails, return an empty array.

  5. If the file exists, start a do block. This allows you to do a series of actions, any of which may fail. As soon as one fails, the program drops down to the catch block, ignoring the rest of the do block.

  6. Try to read Data from the file. This operation may fail, so the try keyword tells your code to make an attempt and use the catch block if there’s a problem. The Data type is a general type to store any data.

  7. If you got Data from the file, create a JSONDecoder and use its decode(_:from:) to convert it. The first argument tells the decoder the data type to return — in this case, an array of Movie objects. The self property of a type returns the type itself. Again, this can fail, especially if the JSON doesn’t match the object structure, so use try.

  8. Next, sort movies using a KeyPathComparator. This lets you supply one of the properties of the object as a key path to use in the sort. \. is the prefix for a key path, so this supplies the title of each movie to the sorter. Return the sorted array.

  9. Finally, if anything went wrong, print the error message and return an empty array. The catch has an automatic error argument. Printing the error is a good way to detect mismatches between your data structure and the JSON.

Phew! That was intense, but now you can finally start using the data.

Reading the JSON

Open ViewController.swift and add this property at the top:

var movies: [Movie] = []

This sets up the array to hold the movies, starting it off as an empty array.

Next, delete representedObject. You won’t use it in this app.

Finally, replace the // Do any additional setup line with:

movies = Movie.readBundleData()
print(movies.count)

This calls the Movie method you wrote for reading the data file and, as a confirmation, prints the number of elements in movies.

Run the app now. There’s nothing visually different, but check the Xcode console:

Reading the movies data.
Reading the movies data.

After all that work, you have over 28,000 movie records in your app. Now, it’s time to display them.

Storyboarding

There’s been a lot of coding so far with little visual result, but finally you get to the user interface.

Open Main.storyboard and take a look around:

Main.storyboard
Main.storyboard

Down the left, the outline view shows three scenes. Use the disclosure arrows to expand them so you can see the contents. Application Scene holds the menu bar. Window Controller Scene has a window and a link to View Controller Scene. It holds the view controller, which contains the view. The view is where you’ll add your interface elements.

In the graphical layout pane, the plain arrow pointing to the Window shows that it’s the initial window that the app displays when it runs. The window shows the text View Controller because that’s what it contains. The arrow with the squiggle connects the view controller as the window’s content.

Configuring the Window

Start by selecting Window by clicking inside the box labeled Window or by selecting Window Controller Scene ▸ Window Controller ▸ Window in the outline view.

Every window in an AppKit app has a window controller to manage the window, create it from the storyboard, handle its content, display the title and control the size.

Open the Inspectors pane on the right by clicking the sidebar toggle button at the top right or by pressing Command-Option-5. The keyboard shortcut opens the Attributes inspector directly, but if you used the toolbar button, click the button with the sliders to open it.

Change Title to Movies and set Autosave to MoviesWindow:

Setting window attributes
Setting window attributes

The top of the window and the outline view both change to show your new window title. Autosave provides a key so the app can save your window size and location for restoring next time you run the app.

The default window is small and this makes laying out the interface more difficult, so now choose the Size inspector — the next tab along.

Set Width to 800 and Height to 400. Then check Minimum Content Size and enter the same values there:

Sizing the window
Sizing the window

You don’t care how big the window gets, but you don’t want it to get too small.

That messed up the display so drag things around so they don’t overlap. If the graphics get too big to fit in your window you’ll see a floating navigation pane that you can click to move around.

Now that you’ve configured the window, you can move on to the view controller.

Setting Up the View

Click inside the View Controller box or select View Controller Scene ▸ View Controller ▸ View in the outline view. In the Size inspector, set Width to 800 and Height to 400 to make this view the same size as its containing window.

Drag the elements around to avoid any overlaps. And now you can start to add some interface elements!

Inserting a Table View

Click the + button in the toolbar or press Shift-Command-L to open the Library. You’ve used the library before to find SwiftUI views and modifiers. Now it shows AppKit objects instead.

Search for table and drag a TableView into the View:

Inserting a table view.
Inserting a table view.

You only asked for a table view, but the library has inserted a lot more. In the outline view, click the disclosure arrow beside Bordered Scroll View and then Option-click it to expand fully:

Expanded table outline
Expanded table outline

There’s a Table View there, but it’s inside a Clip View which is inside a Bordered Scroll View. This allows your table to scroll and ensures it won’t overflow its designated size.

Inside the table view, you have two columns, each with their own sub-components. And finally, you have two scrollers and a header.

Not a bad result for adding a single object from the library! Now, it’s time to configure the table.

Positioning the View

In SwiftUI, the layout engine works out the optimum size and location, depending on the view type and its contents. With AppKit, you have to explicitly tell the view where to go and what size to be.

But your users can resize the window and the views need to adjust accordingly. To allow for this, you use Auto Layout. With Auto Layout, you provide constraints to set spacing between objects and provide size limits.

This can get tricky when you have multiple views as you’ll see in later chapters, but now, you only have a single view to constrain.

Click anywhere in the new view to select the Bordered Scroll View. Then, click the Add New Constraints button in the bottom toolbar.

Enter 0, 0, 400 and 30 for the top, left, right and bottom spacing to nearest neighbor. Leave the other options unchecked. When your settings look like this screenshot, click Add 4 Constraints:

Adding constraints
Adding constraints

Since this is the only subview in the view, the spacing is to the edge of the parent view. When you click the button, the table jumps into place. Check back to the image at the start of this chapter. This positions the table as expected, leaving space at the right for the movie details and at the bottom for the count.

Run the app now and resize the window to confirm that the table adjusts as expected and that the window never gets too small:

Resizable table
Resizable table

It may feel like nothing’s happening yet, but you’re making great progress.

Ignore the layout constraint warnings — you’ll fix them when you configure the table columns.

Setting Table Columns

The final table has three columns showing the title, year and rating for each movie. The default table comes with two columns, so now you’ll fix that.

You need to configure the table itself, but it can be difficult to select it when it’s embedded in other objects. Hold down Shift and right-click in the table view to see a menu of the nested components:

Selection menu.
Selection menu.

Select Table View, open the Attributes inspector on the right and change Columns to 3:

Setting the number of columns.
Setting the number of columns.

Visually, nothing has changed because the third column is outside the box, but you can see a new Table Column in the outline view.

Select AutomaticTableColumnIdentifier.0 in the outline view and, in the Attributes inspector, set its Title to Title. Click the next inspector icon to get to the Size inspector and set Width to 200:

Setting the size for the Title column.
Setting the size for the Title column.

You can set the size by dragging, but this can have unintended consequences due to the difficulty of selecting exactly the right view, so typing in a size is a more reliable method.

In the same way, give AutomaticTableColumnIdentifier.1 a title of Year and a Width of 100.

The third column has started to appear. Give it a title of Rating and a width of 50.

Now that you see all the columns, it’s time to get rid of the layout warnings. Click the yellow arrow beside View Controller Scene in the outline view. This slides sideways to show a new pane with the three Table View Cells listed as needing constraints.

Click each one in turn and, in the Size inspector, change Layout to Autoresizing Mask:

Changing table cell layout.
Changing table cell layout.

This tells the individual table cells to use an older layout style and expand to fill the available space. Xcode doesn’t detect that you’ve fixed these errors. The fastest way to get rid of the yellow marker is to save, then quit and restart Xcode.

Preparing the Table for Data

Now that you’ve set the number of columns and their titles and widths, there are only a few more details to add in the storyboard before you jump back to the code and make the data appear.

First, each column needs an identifier so your code knows which piece of data to provide. Select the Title column in the outline view, or Shift-right-click in the first column and select it from the menu. Open the Identity inspector from the icon or by pressing Command-Option-4.

This column has a preset Identifier of AutomaticTableColumnIdentifier.0, which is descriptive but not informative. Change this to TitleColumn:

Setting the column identifier.
Setting the column identifier.

Next, change the Year column’s Identifier to YearColumn and set the Rating column’s to RatingColumn.

That identifies each column, but you also need to identify the cell inside the column. This allows the code to place the valid data in the correct view.

Shift-right-click in the cell at the top of the Title column where it shows Table View Cell. Choose Table Cell View from the menu:

Selecting the table view cell.
Selecting the table view cell.

In the Identity inspector, you’ll know you chose the correct one if the Class is NSTableCellView. Set the Identifier to TitleCell:

Setting the cell identifier
Setting the cell identifier

Do the same with the cells at the top of the other two columns, setting their identifiers to YearCell and RatingCell respectively.

You’re nearly finished in the storyboard, but you still have to link the table to your code.

Select the table view using whatever method you prefer. Open the Connections inspector by clicking the seventh icon in the Inspectors toolbar or by pressing Command-Option-7.

In the Outlets section, drag from the circle beside dataSource to the View Controller in the outline view. Do the same for delegate. The Connection inspector now shows these links:

Table view connections
Table view connections

Finally, Option-click ViewController.swift in the Project navigator to open it in a separate editor beside the storyboard. Add some blank lines before the movies property declaration.

Control-drag from Table View in the storyboard’s outline to this blank space and release the mouse button when you see the Insert Action or Outlet tooltip:

Connecting the table view.
Connecting the table view.

Enter moviesTableView as the Name and confirm the other settings are as shown here:

Creating an outlet
Creating an outlet

Click Connect to add this code to ViewController:

@IBOutlet weak var moviesTableView: NSTableView!

There’s a lot of detail in this line:

  1. @IBOutlet marks this as an Interface Builder outlet — an object created in the storyboard that your code can now address.
  2. A weak var defines a property that won’t get stuck in memory. When your view controller closes, it frees the memory allocated to this property.
  3. The property type is NSTableView.
  4. This is an optional but the exclamation mark force-unwraps it. Xcode promises to have created the table view from the storyboard by the time you need it. As a general rule, don’t use force-unwrapping, but if Xcode puts it in for you, then it’s OK.

You may not believe it, but you’re very close to showing data now!

Adding a Data Source and Delegate

You’re finished with Main.storyboard for this chapter, so close its editor.

Select ViewController.swift in the Project navigator and add a new Swift File called TableData.swift.

Replace the contents of the new file with:

// 1
import AppKit

// 2
extension ViewController: NSTableViewDelegate, NSTableViewDataSource {
  // methods go here
}

This is where your table gets its data:

  1. You’re using several AppKit classes here, so import AppKit to start.
  2. This is an extension of ViewController, so it’s still part of the same class but separated out for readability. It states that ViewController conforms to the NSTableViewDelegate and NSTableViewDataSource protocols. In the storyboard, you set ViewController as the dataSource and delegate for the table view, so this is the code side of that.

These two protocols provide data to the table, configure the display and respond to any user interactions.

It sounds like NSTableViewDataSource provides the data and NSTableViewDelegate detects events, but the distinction isn’t as clear as that, so you’re using a single extension to supply both.

Next, you’ll add the protocol methods to populate the table.

Providing the Methods

In place of // methods go here, start typing number until you see this autocomplete suggestion:

numberOfRows autocomplete
numberOfRows autocomplete

Press Return to accept the suggestion, and replace the code placeholder with:

movies.count

This is a method in NSTableViewDataSource. The argument is the table view (useful if your view has more than one table), and it returns the number of elements in movies.

The next method returns the view to show in each cell. Add some blank lines below the last method and start typing viewfor to get these suggestions:

viewForRow autocomplete
viewForRow autocomplete

Select tableView(_ tableView:viewFor:row:).

This is an NSTableViewDelegate method that returns the view to show in the specified column and row, but there are some oddities in the arguments.

Swift has a feature where any function argument can have two labels: The caller uses the first one and the method uses second one. For the second argument, the caller uses viewFor, but inside the method, you’ll access it with tableColumn.

The first argument uses an underscore as the external label. This means that the caller doesn’t have to supply a label at all. Using different argument labels is a great way to make your code readable. You can write methods that read like almost like a sentence when called, but still have logical argument names inside the method.

Next, replace the code placeholder with:

// 1
guard let columnID = tableColumn?.identifier else {
  return nil
}

// 2
let movie = movies[row]
var cellID = ""
var cellText = ""

// 3
// get data for each column and cell

// 4
let cellIdentifier = NSUserInterfaceItemIdentifier(cellID)
// 5
if let cell = tableView
  .makeView(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView {
  // 6
  cell.textField?.stringValue = cellText
  return cell
}

return nil

That’s a lot of code!

  1. First, check that the column has an identifier. You set these in the storyboard. If there’s no identifier, return nil and nothing appears in this cell.
  2. Get the movie that corresponds to this row number. Define two variables to hold the cell’s identifier and text.
  3. You’ll add code to set these variables in a minute.
  4. cellID is a String. This converts it into an NSUserInterfaceItemIdentifier, which is what the next line expects.
  5. makeView(withIdentifier:owner:) is an NSTableView method that returns a view with the correct identifier. The view loads from the storyboard originally, but once a cell view has scrolled out of sight, this method re-uses it, which makes NSTableView very efficient even with lots of data. This gets the view, and if it’s an NSTableCellView, sets cell.
  6. If you make it to here, cell is a non-optional NSTableCellView. If it has a textField, set its stringValue to whatever went into cellText and return it.
  7. If anything went wrong, return nil.

Well done for getting through that. You’re nearly there.

Finally, replace // get data for each column and cell with:

// 1
switch columnID.rawValue {
// 2
case "TitleColumn":
  cellID = "TitleCell"
  cellText = movie.title
// 3
case "YearColumn":
  cellID = "YearCell"
  cellText = movie.year
// 4
case "RatingColumn":
  cellID = "RatingCell"
  cellText = "\(movie.rating)"
// 5
default:
  return nil
}

This is where you work out what to put in each cell:

  1. columnID is another NSUserInterfaceItemIdentifier. In the previous chunk, you created one of these from a string. You can do the reverse and read its rawValue to get the string back out. Since there are several possibilities, switch is a good choice for stepping through the options.
  2. If the column identifier is TitleColumn, then the matching cell identifier is TitleCell. The data to show in the cell is the title of the movie for this row.
  3. The YearColumn works the same, since movie.year is a string.
  4. movie.rating is a Double, so you use string interpolation to convert it into a String.
  5. Every switch must be exhaustive, so it can handle every possibility. The default block deals with any other columnID and returns nil.

And with this in place, you’re ready to see your table in action.

Viewing the Table

Press Command-R to run the app:

Table view with data
Table view with data

And there it is — your table showing over 28,000 rows!

If everything went well, you’ll see all the data with three columns for each row. But there was a lot to set up, so maybe you don’t see what you expect.

If this happens to you (and it happens to everybody at some stage), here are the things to check in Main.storyboard:

  • Confirm that ViewController is the dataSource and delegate for Movies Table View.
  • Check the Column Identifiers for each column.
  • Make sure you set the Cell Identifiers on the correct subviews. They are for the NSTableCellViews.

With those fixed, run the app again to see your data appear. You can scroll up and down the list, resize columns, even move columns. This chapter was a lot of work, but you made it. Congratulations!

Key Points

  • AppKit is an alternative layout framework for macOS apps.
  • You design the layout visually using a storyboard and use auto layout to position components.
  • JSON is a commonly used data format that you can transform into Swift objects.
  • AppKit tables are a great way to display large amounts of data.

Where to Go From Here

In the next chapter, you’ll add more functionality to your table. Users will be able to search, sort and see the selected movie’s details.

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.