Improving Storyboard Segues With IBSegueAction

In this iOS tutorial, you’ll learn how to use IBSegueAction for storyboard segues. You’ll understand the advantages and disadvantages of this new technique. By Chuck Krutsinger .

Leave a rating/review
Download materials
Save for later
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Introducing IBSegueAction

Starting with iOS 13, Apple introduced a new way to handle a segue, the @IBSegueAction attribute. The new approach completely reverses the responsibility for creating the endpoint view controller.

With prepare(for:sender:), UIKit creates the view controller and passes the instance to the starting point view controller’s prepare(for:sender:) for configuration. With an IBSegueAction, you create the view controller fully configured and pass it to UIKit for display. After you finish refactoring RazeNotes to use IBSegueActions, you’ll be able to see the advantages.

There are some requirements you must meet for an IBSegueAction:

  1. As you’ve already seen, you must always prefix the IBSegueAction with the @IBSegueAction attribute.
  2. The IBSegueAction must accept an NSCoder argument. It can also accept a sender argument of type Any? and a segue identifier String argument. The segue identifier is seldom needed when using IBSegueAction, because the segue is directly linked to the IBSegueAction in the storyboard. As you’ll see, Xcode’s Interface Builder will generate the method for you.
  3. The endpoint view controller must have an init that takes the coder argument passed to the IBSegueAction. You can pass in as many other arguments as you need, but you must pass in at least the coder. This will become clear as you refactor.
  4. To create your own init, you’ll need to override the required init?(coder:).

All this will make more sense when you do the refactoring. Speaking of which, time to refactor!

Adding @IBSegueAction to RazeNotes

The first thing to do is delete prepare(for:sender:). UIKit will no longer call prepare(for:sender:), and it will produce some compiler errors if you leave it in.

In Xcode, open NotesListViewController.swift. Find prepare(for:sender:) and delete it.

Delete prepare for sender from NotesListViewController

Open EditNoteViewController.swift. At this point, the view controller is only using the inherited initializer. You’ll need an initializer that accepts a note and an optional title. So, the signature you need is init(note:title:coder:).

Before you can declare that new initializer, Swift will require you to override required init?(coder:) even though it won’t be used by the IBSegueAction. Copy and paste the code below into EditNoteViewController to override that required initializer:

required init?(coder: NSCoder) {
  fatalError("init(coder:) is not implemented")

Since you aren’t going to use this initializer in your segues, it deliberately causes a fatal error. This error can warn you during development if you miss a step in handling the IBSegueAction.

Time to add the initializer you will use. Copy and paste this initializer above the previous one:

init?(note: Note, title: String = "Edit Note", coder: NSCoder) {
  self.note = note
  super.init(coder: coder)
  self.title = title

Here, the new initializer is:

  1. Setting the value of the note. This is first since non-optional properties of this class must have a value before calling the superclass initializer.
  2. Calling the UIViewController superclass initializer and passing it the coder so it can layout the view from the storyboard contents.
  3. Setting the title declared in the UIViewController. Note that the title has a default value if the argument is not used. The title is set to last because it’s part of the superclass and can’t be set until after calling the superclass initializer. Xcode will give you an error if you try to set the title before you call the superclass initializer.

Now, you can make some changes that you couldn’t do when using prepare(for:sender:). Replace var note: Note! by copying and pasting the code below.

private let note: Note

The note never changes during the life of EditNoteViewController, so it should be a let. The note should be private since no other class should be setting its value. Finally, you don’t have to force unwrap the note with an !. You can let Swift enforce the initialization of the property.

Declaring properties private and constant is one of the advantages of IBSegueAction. With prepare(for:sender:), you had to set the note of EditNoteViewController after UIKit initialized the view controller. This made compromises necessary.

Open Main.storyboard, and then open NotesListViewController.swift in the assistant editor. Click the newNoteSegue, which will highlight that segue in blue in the canvas. Control-drag from the highlighted segue to right after the viewWillAppear(). Name the method makeNewNoteViewController. Accept the default of None for arguments since you won’t be using the optional sender or identifier arguments.

Add makeNewNoteViewController IBSegueAction

Replace the method that Xcode inserted with the following code:

@IBSegueAction func makeNewNoteViewController(_ coder: NSCoder) 
    -> EditNoteViewController? {
  let newNote = Note()
  notesRepository.add(note: newNote)
  return EditNoteViewController(note: newNote, title: "New Note", coder: coder)

makeNewNoteViewController(_:) replaces the “newNoteSegue” case in prepare(for:sender)‘s switch. However, makeNewNoteViewController(_:) doesn’t have to use a String literal to determine what to execute.

Here is what the method does:

  1. Create a new empty note.
  2. Save the note in the notes repository.
  3. Create and return an instance of EditNoteViewController with the new note and a title passed in. The coder is also passed in, as required by IBSegueAction.

Do the same for the edit note segue. Control-drag from the bottom segue to right below the method you just created. Name the new makeEditNoteViewController. Replace the method that Xcode inserted with the following code.

@IBSegueAction func makeEditNoteViewController(_ coder: NSCoder) 
    -> EditNoteViewController? {
  guard let selectedRow = notesTableView.indexPathForSelectedRow?.row else {
    return nil
  let note = notesRepository[selectedRow]
  return EditNoteViewController(note: note, coder: coder)

Here is what this method does:

  1. Determine the selected row. Return nil if there is none.
  2. Use the row number to fetch the correct note from the note’s repository.
  3. Create and return an instance of EditNoteViewController with the fetched note. As before, the coder is also passed in.

Build and run. The app should work just like before. Tap the New Note button to create a new note. When you tap Back, you’ll see the new note in the list. You can tap any note and edit its contents. Tap Back and then reopen the note to see that your edits are still there.

Build and run the finished app to see IBSegueActions work

Time to compare the two approaches.

Comparing IBSegueAction vs prepare(for:sender:)

Here are the advantages of using IBSegueAction:

  • Cleaner code: Each segue can have its own IBSegueAction, resulting in code that is easier to design and maintain.
  • No switching on string constants: prepare(for:sender:) requires you to switch on segue.identifier values that can get out of sync with the identifier properties in the storyboard file.
  • Better encapsulation: The properties can now be private since the value can be set by the initializer and not after initialization inprepare(for:sender:).
  • Immutability: The required properties can be let constants when appropriate since the value is set by the initializer.
  • Less casting: There’s no need to cast the sender.destination to an EditNoteViewController to configure its properties. You create an instance of the view controller type you need.
  • Easier to test: Since you are not relying on UIKit to create your view controller instances, it will be much easier to create tests for your view controllers.

There is one disadvantage to using IBSegueAction: It is only available for iOS 13 or later. So, if your app must run on earlier versions of iOS, you’ll have to wait to start using this new approach.