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

15. Creating the Edit Interface
Written by Sarah Reichelt

In the previous chapter, you created a contextual menu for the movies table. One of the menu items is for editing a movie, but at the moment, it doesn’t do anything.

Over the next two chapters, you’ll add and configure a new window for doing these edits, with edit fields for the data like the movie title and year, plus a table for listing and editing the movie principals.

When you finish, the edit window will look like this:

Final edit window.
Final edit window.

Adding a New Window

Launch Xcode and open your MovieTables project or get the starter project from the downloads for this chapter.

The first task is to add a new window controller and view controller in the storyboard. Open Main.storyboard and then open the Library using the + button in the toolbar or by pressing Shift-Command-L.

Search for win and drag a Window Controller into any blank area in the storyboard. This adds a Window Controller with a window and a connected View Controller with a view.

Select the window and press Command-Option-5 to open the Attributes inspector. Set its Title to Edit. The window and view are too small, so switch to the next inspector along — the Size inspector — and set the Width to 600 and Height to 400. Press Tab or Return to process your new size, then check Minimum Content Size and make sure the same numbers appear there.

How do you decide what sizes to set? Looking at the design, the table is the widest component. It needs three columns, each of which will be between 150 and 200 pixels wide. Rounding that to a neat number gives a width of 600. For the height, you want to use a fraction of the width. Two-thirds of the width gives 400, which looks good. You may need to change this later as the design develops, but this provides a decent starting point.

Next, select the View Controller ▸ View and set its width and height to 600 and 400 to match.

This View Controller needs its own class. Select TableData.swift in the Project navigator and press Command-N to create a new file. This time, choose macOS ▸ Source ▸ Cocoa Class. This gives a file containing a subclass of a known AppKit class.

Click Next and set the Class to EditViewController. Choose NSViewController from the Subclass of: popup. Uncheck Also create XIB file for user interface. You’ll use Main.storyboard for the design, so you don’t need a separate design file. Make sure Language is set to Swift and press Next, then Create:

Adding a view controller.
Adding a view controller.

This gives your project a new NSViewController class and now you can set it as the class for the newly added View Controller.

In Main.storyboard, select the new View Controller. Use the outline view to be sure you select the controller and not its view. Press Command-Option-4 to open the Identity inspector and use the Class popup to change it to EditViewController:

Assigning the view controller class.
Assigning the view controller class.

When you do this, the names in the outline view change too, making it easier to identify this view controller from now on.

Segueing to the New Window

The navigation link between two view controllers is a segue (pronounced SEG-way). To create a segue, position the objects in the storyboard so you can see the original View Controller and the new Edit window’s Window Controller. Click in the bar at the top of the View Controller so you can see its buttons. Control-drag from the View Controller icon into the new Edit Window Controller, not the Edit View Controller:

Creating a segue.
Creating a segue.

When you release the mouse button, you’ll see a menu. Choose Show as the segue style.

This draws a line between the two objects, with an icon in the middle — this represents the segue, or in full, the NSStoryboardSegue. Click the icon and press Command-Option-5 to open the Attributes inspector for the segue. Set the Identifier to showEditWindow:

Identifying the segue.
Identifying the segue.

Now you have a name you can use to run this segue and show the new window.

Open ViewController.swift and scroll or jump to editMovie(_:). Replace the print line with:

performSegue(withIdentifier: "showEditWindow", sender: movie)

This activates the segue, sending in the clicked movie. The movie doesn’t do anything yet, but you’ve done enough to test the transition.

Run the app, right-click any row in the table and select Edit Movie…:

Segueing to the Edit window.
Segueing to the Edit window.

The new window appears! Now, you need to send the movie data to your new window.

Preparing Data for the Edit Window

When you initiate a segue, it triggers a method called prepare(for:sender:). You can override this method to customize what happens during the transition. At this point, the segue knows where it’s heading, so this is a great place to set some properties on the destination.

Start by opening EditViewController.swift. It needs the Movie object for editing, but there’s a problem. Movie is a class and as such, it’s a reference type. If you passed a movie to EditViewController, you’d actually be passing a reference to that movie — its address in your computer’s memory — not the values in the movie properties. When your edit window changed any properties, these changes would apply directly to the original, but you want to send a copy, so users can cancel their edits and leave the original unchanged.

One way round this is to use JSON encoding to pass the movie to EditViewController as Data and decode it there to get a completely new copy.

Add these properties to EditViewController:

// 1
var movie: Movie?

// 2
var originalMovieData: Data? {
  // 3
  didSet {
    // 4
    if let originalMovieData {
      // 5
      movie = try? JSONDecoder().decode(Movie.self, from: originalMovieData)
      // 6
      print("In EditViewController")
      print(movie)
    }
  }
}

Stepping through these:

  1. The movie property stores an optional Movie.
  2. originalMovieData is optional Data.
  3. Add a property observer to originalMovieData to detect whenever it’s set.
  4. Check if there’s any data in the optional.
  5. If there is, try to decode the JSON data into a Movie and use it to set the movie property.
  6. Print some debug information. This shows a warning but you can ignore it since this line is only temporary.

This is rather long-winded but it solves the problem.

Passing the Data

Next, open ViewController.swift and add this new method underneath editMovie(_:):

// 1
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
  // 2
  guard segue.identifier == "showEditWindow" else {
    return
  }

  // 3
  guard
    let movie = sender as? Movie,
    let movieData = try? JSONEncoder().encode(movie)
  else {
    return
  }

  // 4
  print("Leaving ViewController")
  print(movie)

  // 5
  if
    let windowController = segue.destinationController as? NSWindowController,
    let editViewController = windowController
      .contentViewController as? EditViewController {
    // 6
    editViewController.originalMovieData = movieData
  }
}

What does this do?

  1. Provide an override method for your segues.
  2. Confirm this is the showEditWindow segue.
  3. Check the segue’s sender is a Movie and try to encode it into JSON data. You set the sender value when you called performSegue(withIdentifier:sender:).
  4. Print some debug information.
  5. Now you have to work your way through the view hierarchy. The segue’s destination is a window controller and that window controller has a contentViewController . You need to make sure this is an EditViewController so you can set its properties.
  6. If the segue ends up at the correct type of view controller, set its originalMovieData, which then sets its movie property.

The edit window has no UI yet, but there’s enough in place to test the data flow. Run the app, right-click any movie and select Edit Movie…:

Movie for editing
Movie for editing

Check the Xcode console. The key thing is the hexadecimal numbers shown for each movie (yours will be different to the ones in the screenshot). These are the memory addresses for the two movies and they’re not identical, which confirms you’ve got two distinct Movie objects. This is why you went through all this encoding and decoding. If you’d sent movie directly to EditViewController, the addresses would have been the same and any edits would have changed the main data array.

Now you’ve proved that the data transfer works, delete the two print lines in ViewController.swift and in EditViewController.swift.

With the data transfer in place, you can start work on the interface.

The Editing Interface

The edit window has three main sections. The top section is for the single chunks of data: title, year etc. The middle is a table for the list of principals. And finally the bottom has the Cancel and Save Changes buttons.

Open Main.storyboard and scroll to see the Edit View Controller. Starting at the bottom, open the Library and search for button.

Drag a Push Button into the bottom left corner until you see the two blue alignment guides. Place the button with its bottom left corner at the intersection of these lines:

Aligning a button.
Aligning a button.

Repeat the process, dragging another Push Button to the bottom right corner.

Select the left button and open the Attributes inspector. Set the Title to Cancel. Press Tab or Return to finish editing and Xcode sets the Key Equivalent to Escape:

Cancel button attributes
Cancel button attributes

Select the other button and set its Title to Save Changes. This time you’ll have to set the Key Equivalent yourself, so click in the Key Equivalent box and press Return. This applies the default button appearance.

The new title makes the button wider, so drag it back to the alignment guides.

Select both buttons, click Resolve Auto Layout Issues and choose Reset to Suggested Constraints:

Reset to suggested constraints
Reset to suggested constraints

This pins them both to the bottom and sides, but doesn’t fill in a constraint for the gap in the middle.

Select the Cancel button, Control-drag to the Save Changes button and choose Horizontal Spacing:

Adding horizontal spacing
Adding horizontal spacing

This sets a fixed spacing between them, but you want a more flexible setup.

With the Cancel button still selected, swap to the Size inspector. Click the Edit button in the Trailing Space to: Save Changes constraint and change the Constant popup to greater than or equal to:

Changing the constraint constant.
Changing the constraint constant.

That’s all for the buttons. Now, it’s time to set up the table.

Placing the Table View

Find Table View in the Library and drag it so it aligns with the left alignment guide and clicks into the standard spacing above the Cancel button:

Adding a table view.
Adding a table view.

Drag the right side of the table until it reaches the alignment guide on the right. To fix the table in place, click Add New Constraints. Set the top spacing to 150 and click the lines going left, right and down to turn them red and set them to Standard. Check Width and leave it at 560. Check Height and change it to 190. Then click Add 6 Constraints:

Setting table constraints.
Setting table constraints.

This sets a fixed height for the table, but you want this to be its minimum height, so in the Size inspector, find the Height constraint and use the Edit button to change it to greater than or equal to 190:

Editing table height.
Editing table height.

Do the same for the Width constraint, setting it to greater than or equal to 560.

This fixes the table but leaves it independent of any elements you add later. Setting the minimum height and width fixes the entire view so it can never be smaller than the minimum size. This gets round Xcode’s nasty tendency to adjust view sizes when you close and re-open a project.

To finish the table, give it a label. Drag a Label from the Library into the view. Use the Attributes inspector to set its Title to Principals: and its Font to Text style - Headline:

Principals label
Principals label

Drag the label to above the top left corner of the table, using the alignment guides to position it. Click Resolve Auto Layout Issues and Reset to Suggested Constraints to lock it into place.

This gives a layout warning because it’s missing a trailing constraint. Click the yellow arrow button, then the yellow triangle and select Use Fixed Leading and Resizing Trailing Constraints because this is to the left of the view. Click Confirm and the warning disappears. Click < Structure to get back to the outline view.

All that remains is to build the top section.

Setting up the Text Edit Fields

Checking the screenshot at the start of this chapter, the top section is the most complicated.

Start by adding the edit fields. Open the Library and search for text. Drag a Text Field into the view.

Move it to align the top right corner with the guides near the top right of the view, then drag the left side out until the width is 490:

Positioning the top edit field.
Positioning the top edit field.

Option-drag the field down to create a copy. Make sure it aligns left and right and is the standard distance below the original. Repeat this so you have three edit fields down the display.

Click the middle text field, grab the handle at the right side and drag it left until the width is 100. Duplicate (Option-drag) this shorter field and move it towards the middle until it gets a central alignment guide from the longer text fields. Then, duplicate again and align this one to the guide at the right:

Aligning the text fields.
Aligning the text fields.

Drag to select all five of these fields before clicking Resolve Auto Layout Issues and Reset to Suggested Constraints for the selected views.

Time to see if this has worked. Run the app, right-click any row in the table and select Edit Movie…. Resize the Edit window to see what works:

Testing the Auto Layout.
Testing the Auto Layout.

Nearly everything is good. Hurray for Reset to Suggested Constraints. The only thing not right is the central edit field. It correctly stays centered, but its width grows. The middle row of fields is for run time, year and rating, so they don’t need to expand.

Back in Main.storyboard, things are about to get a bit messy.

Select the middle text field and open the Size inspector using Command-Option-6. The vertical constraints are fine, but the horizontal ones aren’t giving the desired result. The one that aligns the center X to the text field above is great. The problem is the fixed spacing to the text fields on either side. Since these are both set to 95, the text field has to expand to maintain them. Delete both the Trailing Space to: Text Field and Leading Space to: Text Field constraints.

Without these constraints, the text field no longer has enough information to position and resize itself. Fix this by setting a specific width. Click Add New Constraints. Check Width, make sure its value is 100 and click Add 1 Constraint:

Constraining the width.
Constraining the width.

That fixes the middle field, but now the one on the right needs some help. Select it and use Add New Constraints to set its Width to 100.

That gets rid of all the errors and warnings, so now you can label these fields.

Labeling the Edit Fields

The Principals label is set up the way you want, so select it and use Command-D or Option-Drag, whichever you prefer, to duplicate it. Drag the copy up to beside the top edit field. These field labels align to the left of their matching edit fields, so use the Attributes inspector to set the alignment to Right.

Duplicate this field four more times, positioning each copy beside an edit field, aligning the text baselines and spacing the standard distance apart. The baseline shows two alignment guides in the bottom half of the fields:

Note: After you position the labels beside the text fields, you can add a trailing constraint with standard spacing for each label so you don’t need to manually adjust the spacing between the labels and the text fields.

Aligning baselines.
Aligning baselines.

Set the titles of these labels to Title:, Run time:, Year:, Rating:, and Genres:.

Now, you need to set up their Auto Layout constraints. Select all the labels. You can’t drag to select without including the edit fields, so select one and Command-click the other four.

Time for Resolve Auto Layout Issues and Reset to Suggested Constraints for the selected views, which works but leaves you with the yellow warning again.

Click the yellow arrow to see the issues. Click the yellow triangle for each of them, and this time choose Use Resizing Leading and Fixed Trailing Constraints for each one. Even though some are on the left, you want them to stick to their edit fields.

That gets rid of all the warnings, so click < Structure to get back to the outline view, and then run the app. Edit a movie and test resizing the window:

Complete layout
Complete layout

You did it! You worked through all those Auto Layout settings and ended with a great looking Edit window that resizes as expected. That’s never easy. Auto Layout is something you have to work at, but it gives a responsive and flexible result in the end.

Note: It’s easy to get confused in Auto Layout. If things aren’t working, or you get more warnings than expected, click Resolve Auto Layout Issues and Clear Constraints for the entire view controller, then work through them again. You can also check the final project for this chapter and read the constraints set there.

Now, it’s time to start bringing the edit window alive.

Coding the Buttons

With Main.storyboard still open, Option-click EditViewController.swift in the Project navigator to open it in a secondary editor.

Add some blank lines before the last closing curly brace, then Control-drag from the Cancel button into this space. Connect an action called cancelEdits.

Do the same for the Save Changes button, creating an action called saveEdits.

Add this code to cancelEdits(_:):

view.window?.close()

The view controller has a view, and that view can try to access its containing window. If that works, call the window’s close() method. And that’s all the Cancel button needs.

The Save Changes button has more work to do, so add this code to saveEdits(_:):

// 1
view.window?.makeFirstResponder(nil)

// 2
// tell parent view controller to save

// 3
view.window?.close()

Going through these lines:

  1. When you type in a text edit field, the changes aren’t processed until you click out of the field or press Tab or Return. You’ve seen this when editing in the inspectors. This line sets the first responder to nil, which causes any active control to resign its focus. If this is a text field, it triggers the edit processing so the data in the active edit field becomes part of the saved data.
  2. The save process is going to happen in ViewController, which already has methods to save the data and update the display.
  3. Then, use the same technique for closing the window.

Run the app now and edit a movie. There’s nothing new to see, but either of the buttons closes the Edit window. The Escape and Return keys do the same.

Now, it’s time to finish the Save Changes code.

Saving the Edits

In order for EditViewController to call a method in ViewController, it needs access to that view controller.

Close the secondary editor pane, open EditViewController.swift and add this property:

weak var parentVC: ViewController?

This gives EditViewController an optional reference to the main ViewController. It’s a weak reference to avoid the connection hanging around in memory.

You supply this property to EditViewController in ViewController’s prepare(for:sender:) method.

Open ViewController.swift and find that method. Below where you set editViewController.originalMovieData add:

editViewController.parentVC = self

In this case, self is ViewController.

Next, add the ViewController method to do the actual save:

// 1
func saveEdits(for movie: Movie) {
  // 2
  let index = movies.firstIndex {
    $0.id == movie.id
  }

  if let index {
    // 3
    movies[index] = movie
  } else {
    // 4
    movies.append(movie)
  }

  // 5
  searchMovies()
  if selectedMovie?.id == movie.id {
    showSelectedMovie(movie)
  }

  // 6
  dataStore.saveData(movies: movies)
}

This method has a bit going on:

  1. Call the method with a Movie to save. The argument has an external label of for and an internal label of movie to make calling and using both read logically.
  2. Search the movies array for the index of a movie with the matching id.
  3. If the search found an existing movie, replace it in the database.
  4. If this is an unknown movie, append it to the array. The app doesn’t have the ability to add movies, but maybe you’ll decide to add that later. This line future-proofs this method.
  5. Update the display using searchMovies() and update the selected movie display, if appropriate.
  6. Save the edited movies array.

Now EditViewController has a way to access ViewController and ViewController has a method for saving an edited movie.

Open EditViewController.swift and in saveEdits(_:), replace // tell parent view controller to save with:

if
  let parentVC,
  let movie {
  parentVC.saveEdits(for: movie)
}

Both parentVC and movie are optionals, so only call saveEdits(for:) if they both have values.

You have both the buttons completely set up and coded. In the next chapter, you’ll show the data and allow users to edit it.

Making Editing Easier

Before getting into the actual editing, there’s a feature you can add to the main table to make editing faster and easier. How about letting the user double-click a line to open the Edit window?

Open Main.storyboard, scroll to the main window, and use the Shift-right-click menu to select Movies Table View.

Open the Connections inspector (Command-Option-7) and drag from Sent Actions ▸ doubleAction to the View Controller box in the bar above the window:


Connecting the doubleAction.
Connecting the doubleAction.

Release the mouse and choose editMovie: from the list. This is the @IBAction you created for the contextual menu, but there’s no reason why the doubleAction can’t use it too.

Now, run the app and double-click any movie:


Double-click editing.
Double-click editing.

The neat thing about this is that it selects and edits, all with a single user action.

Now you have a great UI set up, ready to display editable data.

Key Points

  • To show a secondary window, add a Window Controller and a View Controller to the storyboard.
  • Use a segue to transition to the new window.
  • Attach data to the segue to send it to the secondary window.

Where to Go From Here

The next step is to show the data in the fields and table. You’ll also want to configure the fields for editing and set up the table for sorting and editing. That’s all in the next chapter.

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.