Initializer Injection with View Controllers

Initializer Injection with View Controllers, Storyboards, and Segues

Initializer Injection with View Controllers
1 Initializer Injection with View Controllers and Storyboards 08:02
2 Initializer Injection with View Controllers, Storyboards, and Segues 07:08
Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

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.

Exploring the Starter Project

Exploring the Starter Project

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.

Initializer Injection with View Controllers, Storyboards, and Segues

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.

Connecting the Segue and the Segue Action

With the segue selected, open the Connections Inspector on the right to verify that the segue and the segue action are connected.

Connecting the Segue and the Segue Action

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.

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy