In the previous episode, you learned that it is possible to use initializer injection in combination with storyboards as of iOS 13 and tvOS 13. We didn't cover segues in that episode, though. That is the focus of this episode. Let's take a look at an example.
Exploring the Starter Project
Fire up Xcode and open the starter project of this episode if you want to follow along. The application displays a collection of images in a table view. Each cell displays a thumbnail and a title. Tapping a cell takes the user to a detail view, showing a larger version of the image. This should look familiar.
The project is a bit different, though. Let's start with the project's main storyboard. A segue connects the Images View Controller scene to the Image View Controller scene. The segue is executed when the user taps a table view cell in the images view controller.
Select the segue and open the Attributes Inspector on the right. The segue's identifier is set to ShowImage and Kind is set to Present Modally.
Open ImagesViewController.swift and navigate to the prepare(for:sender:)
method. This method is invoked every time a segue is executed with the images view controller as the source view controller. The images view controller switches on the segue's identifier. If the segue's identifier is equal to ShowImage
, the images view controller casts the segue's destination view controller to ImageViewController
and sets its image
property.
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.identifier {
case Segue.showImage:
guard
let indexPath = tableView.indexPathForSelectedRow,
let imageViewController = segue.destination as? ImageViewController
else {
return
}
// Configure Image View Controller
imageViewController.image = dataSource[indexPath.row]
// Deselect Row
tableView.deselectRow(at: indexPath, animated: true)
default:
break
}
}
Property Injection
The images view controller uses property injection to inject the Image
object into the image view controller. We covered the pros and cons of property injection in the previous episode. The goal of this episode is to use initializer injection instead of property injection.
Initializer Injection
As of iOS 13 and tvOS 13, initializer injection is possible in combination with storyboards and segues thanks to the introduction of segue actions. A segue action is a method the source view controller defines. It is invoked every time the segue is executed. A segue action accepts an NSCoder
instance and returns a view controller. The NSCoder
instance contains the data loaded from the storyboard to create the destination view controller of the segue.
Adopting initializer injection requires three steps. First, we define a custom initializer for the ImageViewController
class. Second, we define a segue action in the source view controller of the segue. Third, we connect the segue and the segue action in the storyboard.
Defining a Custom Initializer
Open ImageViewController.swift and add an initializer with name init(coder:image:)
. The initializer accepts an NSCoder
instance as its first argument and an Image
object as its second argument. In the initializer, we set the image
property and invoke the inherited init(coder:)
initializer.
// MARK: - Initialization
init?(coder: NSCoder, image: Image) {
self.image = image
super.init(coder: coder)
}
We are also required to implement the init(coder:)
initializer. Because it shouldn't be used to instantiate an ImageViewController
instance, we throw a fatal error in the init(coder:)
initializer.
required init?(coder: NSCoder) {
fatalError("Use `init(coder:image:)` to initialize an `ImageViewController` instance.")
}
Because the initializer of the ImageViewController
class accepts an Image
object, we can make a few improvements. We declare the image
property as a private, constant property and we change its type from Image?
to Image
.
import UIKit
internal final class ImageViewController: UIViewController {
// MARK: - Properties
private let image: Image
...
}
We no longer need to safely unwrap the image
property in the view controller's viewDidLoad()
method.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Fetch Image
dataTask = fetchImage(with: image.fullSizeUrl)
}
Defining a Segue Action
A segue action is a method with the IBSegueAction
attribute applied to it. Open ImagesViewController.swift and define a method with name showImage(coder:sender:segueIdentifier:)
. We define the method as a private method and apply the IBSegueAction
attribute to it. The method defines three parameters, an NSCoder
instance, a sender, the object that initiated the segue, and the identifier of the segue. You can omit the sender and the segue identifier. They are not required. The return type of the showImage(coder:sender:segueIdentifier:)
method is ImageViewController?
.
// MARK: - Actions
@IBSegueAction private func showImage(coder: NSCoder, sender: Any?, segueIdentifier: String) -> ImageViewController? {
}
We can draw inspiration from the prepare(for:sender:)
method. We use a guard
statement to ask the table view for the index path of the selected row. The showImage(coder:sender:segueIdentifier:)
method returns nil
if indexPathForSelectedRow
returns nil
. This should never happen, though. If we return nil
form the segue action, the destination view controller is instantiated using the default init(coder:)
initializer.
// MARK: - Actions
@IBSegueAction private func showImage(coder: NSCoder, sender: Any?, segueIdentifier: String) -> ImageViewController? {
guard let indexPath = tableView.indexPathForSelectedRow else {
return nil
}
}
Before we instantiate the image view controller, we deselect the row the user tapped by calling deselectRow(at:animated:)
on the table view.
// MARK: - Actions
@IBSegueAction private func showImage(coder: NSCoder, sender: Any?, segueIdentifier: String) -> ImageViewController? {
guard let indexPath = tableView.indexPathForSelectedRow else {
return nil
}
// Deselect Row
tableView.deselectRow(at: indexPath, animated: true)
}
To instantiate the image view controller, the images view controller invokes the initializer we implemented a moment ago. The initializer accepts the NSCoder
instance and the Image
object that corresponds with the row the user tapped.
// MARK: - Actions
@IBSegueAction private func showImage(coder: NSCoder, sender: Any?, segueIdentifier: String) -> ImageViewController? {
guard let indexPath = tableView.indexPathForSelectedRow else {
return nil
}
// Deselect Row
tableView.deselectRow(at: indexPath, animated: true)
// Initialize Image View Controller
return ImageViewController(coder: coder, image: dataSource[indexPath.row])
}
Before we can test the implementation, we need to connect the segue and the segue action in the project's main storyboard. Open Main.storyboard and select the segue that connects the Images View Controller scene to the Image View Controller scene. Press Control, drag from the segue to the images view controller, and select the segue action from the menu that pops up.
With the segue selected, open the Connections Inspector on the right to verify that the segue and the segue action are connected.
Open ImagesViewController.swift and remove the prepare(for:sender:)
method. We no longer need it. Build and run the application to test the implementation. Even though the behavior of the application hasn't changed, the implementation has improved quite a bit.
What's Next?
Even though I don't often use segues, it is nice to see that it is possible to use initializer injection in combination with storyboards and segues as of iOS 13 and tvOS 13. Storyboards don't have the best reputation. I hope these small but important UIKit additions help change that.