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:
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:
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:
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:
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.
AppDelegateis a subclass ofNSApplicationDelegateand it handles interactions with the application itself. -
ViewController.swift: In SwiftUI, you create
Viewsthat the app shows inScenes. In AppKit, you create subclasses ofNSViewController. 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:
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:
- To work with AppKit tables, you must use a
class, not astruct. The class conforms toCodable, which means that it works with JSON data. - The
idproperty is a constant so it can never change. - Later on, you’ll let users edit the rest of the movie data, so the other properties are variables.
- 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:
This adds an init method that assigns the arguments to the matching properties.
Note: The
internalkeyword 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:
-
Create an extension of the
Movieclass. This is part of the class, but separating it like this makes it clearer that this is a class method. -
Define a
staticmethod. You call this method on the class itself, not on any particular instance ofMovie. It returns an array ofMovieobjects. -
Start by trying to get the
URL, or file address, of movies.json in the application bundle. -
If this fails, return an empty array.
-
If the file exists, start a
doblock. This allows you to do a series of actions, any of which may fail. As soon as one fails, the program drops down to thecatchblock, ignoring the rest of thedoblock. -
Try to read
Datafrom the file. This operation may fail, so thetrykeyword tells your code to make an attempt and use thecatchblock if there’s a problem. TheDatatype is a general type to store any data. -
If you got
Datafrom the file, create aJSONDecoderand use itsdecode(_:from:)to convert it. The first argument tells the decoder the data type to return — in this case, an array ofMovieobjects. Theselfproperty of a type returns the type itself. Again, this can fail, especially if the JSON doesn’t match the object structure, so usetry. -
Next, sort
moviesusing aKeyPathComparator. 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. -
Finally, if anything went wrong, print the error message and return an empty array. The
catchhas an automaticerrorargument. 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:
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:
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:
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:
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:
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:
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:
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:
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:
Select Table View, open the Attributes inspector on the right and change Columns to 3:
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:
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:
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:
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:
In the Identity inspector, you’ll know you chose the correct one if the Class is NSTableCellView. Set the Identifier to TitleCell:
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:
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:
Enter moviesTableView as the Name and confirm the other settings are as shown here:
Click Connect to add this code to ViewController:
@IBOutlet weak var moviesTableView: NSTableView!
There’s a lot of detail in this line:
-
@IBOutletmarks this as an Interface Builder outlet — an object created in the storyboard that your code can now address. - A
weak vardefines a property that won’t get stuck in memory. When your view controller closes, it frees the memory allocated to this property. - The property type is
NSTableView. - 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:
- You’re using several AppKit classes here, so import AppKit to start.
- This is an extension of
ViewController, so it’s still part of the same class but separated out for readability. It states thatViewControllerconforms to theNSTableViewDelegateandNSTableViewDataSourceprotocols. In the storyboard, you setViewControlleras thedataSourceanddelegatefor 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:
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:
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!
- First, check that the column has an identifier. You set these in the storyboard. If there’s no identifier, return
niland nothing appears in this cell. - Get the movie that corresponds to this row number. Define two variables to hold the cell’s identifier and text.
- You’ll add code to set these variables in a minute.
-
cellIDis aString. This converts it into anNSUserInterfaceItemIdentifier, which is what the next line expects. -
makeView(withIdentifier:owner:)is anNSTableViewmethod 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 makesNSTableViewvery efficient even with lots of data. This gets the view, and if it’s anNSTableCellView, setscell. - If you make it to here,
cellis a non-optionalNSTableCellView. If it has atextField, set itsstringValueto whatever went intocellTextand return it. - 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:
-
columnIDis anotherNSUserInterfaceItemIdentifier. In the previous chunk, you created one of these from a string. You can do the reverse and read itsrawValueto get the string back out. Since there are several possibilities,switchis a good choice for stepping through the options. - If the column identifier is TitleColumn, then the matching cell identifier is TitleCell. The data to show in the cell is the
titleof themoviefor this row. - The YearColumn works the same, since
movie.yearis a string. -
movie.ratingis aDouble, so you use string interpolation to convert it into aString. - Every
switchmust be exhaustive, so it can handle every possibility. Thedefaultblock deals with any othercolumnIDand returnsnil.
And with this in place, you’re ready to see your table in action.
Viewing the Table
Press Command-R to run the app:
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
ViewControlleris thedataSourceanddelegatefor 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.