17

Improving Storyboard Segues With IBSegueAction [FREE]

 3 years ago
source link: https://www.raywenderlich.com/9296192-improving-storyboard-segues-with-ibsegueaction
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Segue-Image-629x500.png

In UIKit, a segue is an object that defines a transition between two view controllers in a storyboard file. It has nothing to do with a two-wheeled scooter. The only thing a segue and a Segway have in common is that they are homophones. No more, no less!

mascot_swift-segue.png

This tutorial covers a new way of implementing a UIKit segue called IBSegueAction . Introduced in iOS 13, an IBSegueAction lets you create view controllers before UIKit displays them.

In this tutorial, you’ll create two segues using the pre-iOS 13 approach based on prepare(for:sender:) . Then, you’ll refactor those same segues using @IBSegueAction to create the endpoint view controllers. You’ll learn about the advantages that IBSegueAction s offers over the previous approach as well as the disadvantages. Well, the one disadvantage.

Note : Before you continue, this tutorial assumes you are familiar with storyboards and table views. If you aren’t, you may want to start with iOS Storyboards: Getting Started and return to this tutorial afterward.

Getting Started

Begin by downloading the project materials using the Download Materials button at the top or bottom of this tutorial. Then open RazeNotes.xcodeproj in the starter folder.

The app, RazeNotes , is a simple note-taking app. It comes preloaded with some fun notes, but feel free to add your own.

Build and run. Tap the new note button in the top-right corner and each of the notes in the list. You’ll notice nothing happens; that’s because the app doesn’t have any segues. Yet.

To make it work, you’ll be adding segues to the new note button and to the items on the list.

Build-and-run-starter-project.gif

Exploring Segues

Before adding functionality to RazeNotes, it would be good to understand more about segues. In UIKit, a segue is an object that defines a transition between two view controllers in a storyboard file. An action — such as tapping a button, performing a gesture or tapping on a table cell row — triggers the segue. The endpoint of the segue is the view controller you want to display.

The diagram below illustrates two segues connecting the same two view controllers. The segue arrow points toward the endpoint.

Segue-ties-two-view-controllers-together.png

The most common is the Show segue, which puts a view controller modally on top of the current view controller. That’s what you’ll implement in this tutorial.

But there are several other types of segues for different kinds of transitions. The menu below shows all the different types of segues, including some that are deprecated. You shouldn’t add the deprecated segue types to new projects. They’re there for backward compatibility.

Menu-of-segue-types.png

Note :Apple discusses all the different types of segues in its documentation on segues .

In most cases, you’ll need to configure the segue’s endpoint view controller before it displays. Before iOS 13, UIKit created the endpoint view controller, and then you configured it in the starting view controller’s prepare(for:sender:) . As a result, you had to configure it after it was already initialized.

That has some downsides that you’ll be able to experience once you’ve implemented the segues in both fashions.

Making RazeNotes Segues Work Before iOS 13

swift-cooking-1-500x500.png

Time to do some development! In this section, you’ll handle the new note and edit note segues using prepare(for:sender:) , the traditional method of handling segues. By the end of this section, you’ll have a working RazeNotes app!

Start by opening Main.storyboard . Then, click the RazeNotes Scene in the storyboard. This step will be easiest if you open NotesListViewController.swift in the assistant editor by choosing Editor ▸ Assistant from the Xcode menu. The editor on the right should open to NotesListViewController.swift .

If the assistant editor opens to some other file, then select Automatic at the top-right corner of the editor window and choose Top-Level Objects ▸ NotesListViewController.swift .

Open-main-storyboard-and-assistant-editor-1.gif

To reveal the New Note button, expand the hierarchy of controls in the RazeNotes Scene of the Document Outline; it’s on the left side of the storyboard screen. To create a segue for a new note, Control-drag from the New Note button in the Document Outline to the Edit Note view controller. From the menu that pops up, choose the Show segue type.

Add-new-note-segue.gif

Select the new segue. Then open the Attributes inspector so that you can fill in the Identifier attribute with newNoteSegue . You’ll use the identifier later to determine which segue is triggered.

Set-new-note-segue-identifier.gif

It’s the same process to create a segue for editing a note. Control-drag from the notesCell to the Edit Note view controller in the storyboard Document Outline on the left side. From the menu that pops up, choose the Show segue type again.

Add-edit-note-segue.gif

Click the new segue and open the Attributes inspector so that you can fill in the Identifier attribute with editNoteSegue .

Set-edit-note-segue-identifier.gif

Time to make those segues work. In the assistant editor, NotesListViewController.swift is already open. Add prepare(for:sender:) to the file right after viewWillAppear(_:) . Just type prepareforsend , and let Xcode autocomplete the definition.

Add-prepare-for-sender-method.gif

Paste over the empty prepare(for:sender:) with the following code:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  //1
  guard let destinationViewController = segue.destination 
      as? EditNoteViewController else {
    return
  }

  //2    
  switch segue.identifier {
  //3
  case "editNoteSegue":
    guard let selectedRow = notesTableView.indexPathForSelectedRow?.row else {
      return
    }
    destinationViewController.note = notesRepository[selectedRow]
      
  //4
  case "newNoteSegue":
    let newNote = Note()
    notesRepository.add(note: newNote)
    destinationViewController.note = newNote
    destinationViewController.title = "New Note"

  default:
    break
  }
}

To make the two segues work, prepare(for:sender:) is:

  1. Casting the segue.destination as an EditNoteViewController since both segues require that type.
  2. Switching on the segue’s identifier to determine how to configure the destinationViewController . Note that switching on a String like this is one of the downsides of this approach. The identifier must exactly match the identifier attribute set in the storyboard. This can be a source of errors.
  3. For the editNoteSegue:
    notesRepository
    destinationViewController
    
  4. For the newNoteSegue:
    Note
    notesRepository
    destinationViewController
    destinationViewController
    

Build and run. 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 also tap any note to read or edit its contents. Tap Back and then reopen the note to see if your edits took.

Build-and-run-to-see-segues-work.gif

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 IBSegueAction s, 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.gif

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) {
  //1
  self.note = note
  //2
  super.init(coder: coder)
  //3
  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.gif

Replace the method that Xcode inserted with the following code:

@IBSegueAction func makeNewNoteViewController(_ coder: NSCoder) 
    -> EditNoteViewController? {
  //1
  let newNote = Note()
  //2
  notesRepository.add(note: newNote)
  //3
  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? {
  //1
  guard let selectedRow = notesTableView.indexPathForSelectedRow?.row else {
    return nil
  }
    
  //2
  let note = notesRepository[selectedRow]
  //3
  return EditNoteViewController(note: note, coder: coder)
}

Here is what this method does:

nil
EditNoteViewController

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-finished-app.gif

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 in prepare(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.

Where to Go From Here?

You can download the completed version of the RazeNotes project by using the Download Materials button at the top or bottom of this tutorial.

IBSegueAction provides many advantages over the previous approach to UIKit segues. If you’re able to target only newer devices, this is the recommended approach to implementing segues.

If you want to learn more about segues, check out iOS Storyboards: Segues and More .

If you want to learn about SwiftUI, which is Apple’s replacement for UIKit and does view navigation differently, then you may want to read the SwiftUI: Getting Started tutorial or the more in-depth book, SwiftUI by Tutorials .

If you have any questions or comments, please join the forum discussion below!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK