Dependency Injection With View Controllers

Dependency Injection With Storyboards

Dependency Injection With View Controllers
1 Dependency Injection With Storyboards 08:29
2 Dependency Injection With XIB Files Plus 09:36
3 Dependency Injection In Code Plus 07:32
4 Dependency Injection With Tab Bar Controllers Plus 08:08
Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By

Dependency injection is a pattern that's often overlooked, ignored, or discarded by developers in favor of other patterns, such as the singleton pattern. I've talked and written about dependency injection and Swift quite a bit on Cocoacasts.

Developers that are new to dependency injection tend to have a few questions about the practical side of the pattern. They're usually not sure how to implement dependency injection in their projects.

The most common question I receive about dependency injection relates to view controllers. How do storyboards and XIB files fit into the story? In this series, I show you how dependency injection is used in combination with view controllers. We cover view controllers in code, view controllers that are linked to a XIB file, and view controllers that live in a storyboard.

Storyboards

Unlike many other developers, I enjoy using storyboards. The introduction of storyboard references has made working with storyboards more flexible and less daunting. Storyboards aren't perfect, but they make several tasks a lot easier.

The most important limitation of storyboards is the loss of control. You're no longer in charge of instantiating the view controllers of your application. It's a benefit in many ways, but it's an important limitation in the context of dependency injection.

But it doesn't mean you can't adopt dependency injection if you're using storyboards in a project. Because we're not in control of the initialization of the view controllers of the project, initializer injection isn't an option. It isn't possible to inject the view controller's dependencies when it's being initialized.

Initializer injection isn't the only tool in your toolbox, though. Remember from Nuts and Bolts of Dependency Injection in Swift that we define three types of dependency injection:

  • initializer injection
  • property injection
  • method injection

Initializer injection and property injection are the preferred options for view controllers. I've never used method injection in combination with view controllers. It doesn't make much sense.

Because we can't use initializer injection if we're taking advantage of storyboards, we need to rely on property injection. This implies that we need to find a moment, soon after the initialization of the view controller, to inject its dependencies, that is, to set its properties. Remember that dependency injection is nothing more than giving an object its instance variables. View controllers are no different.

Remember that dependency injection is nothing more than giving an object its instance variables. View controllers are no different.

The question is "When can or should we inject the dependencies into a view controller?" The answer is surprisingly simple. Storyboards aren't terribly useful without segues and segues don't make sense without storyboards. The moment a view controller performs a segue to present another view controller, it's given the opportunity to prepare for the segue before it's performed. That opportunity is translated into the invocation of the view controller's prepare(for:sender:) method.

Let me show you how this works with an example. Download the starter project of this episode if you'd like to follow along. The initial view controller of the main storyboard is an instance of the RootViewController class. It shows the user a list of Note instances in a table view. When the user taps a note in the table view, an instance of the DetailViewController class is presented modally, showing the user the details of the selected note.

Dependency Injection in Swift

This features doesn't work yet because the detail view controller doesn't know which note the user wants to see the details of. It's the responsibility of the root view controller to pass the selected note to the destination view controller of the segue, the detail view controller.

To make this work, we need to implement the prepare(for:sender:) method of the RootViewController class.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

}

We ask the table view for the index path of the currently selected row and use that index path to fetch the note the user is interested in. Notice that we use a guard statement to safely unwrap the result of indexPathForSelectedRow. If the result of indexPathForSelectedRow is equal to nil, then there's no need to continue. I usually throw a fatal error in that scenario since we're about to present a DetailViewController instance without a Note instance to display. But that's another discussion.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let indexPath = tableView.indexPathForSelectedRow else { return }

    // Fetch Note
    let note = notes[indexPath.row]
}

We ask the segue for the destination view controller and cast the result to an instance of the DetailViewController class. We use another guard statement to safely cast the destination view controller to an instance of the DetailViewController class. We pass the Note instance to the detail view controller. The detail view controller is ready to present the details of the note to the user.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let indexPath = tableView.indexPathForSelectedRow else { return }

    // Fetch Note
    let note = notes[indexPath.row]

    guard let destination = segue.destination as? DetailViewController else { return }

    // Configure Detail View Controller
    destination.note = note
}

Adding More Safety

While this simple implementation of the prepare(for:sender:) method works fine, I'd like to add some improvements. I recommend naming each segue and making sure you're handling the correct segue in the prepare(for:sender:) method, even for simple user interfaces. Because I don't like random string literals in a project, I define a private, nested enum, Segue, with a static property, NoteDetails.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Types

    private enum Segue {

        static let NoteDetails = "NoteDetails"

    }

    ...

}

We assign the segue identifier to the segue in the main storyboard. Select the segue, open the Attributes Inspector on the right, and set Identifier to NoteDetails.

Setting the Identifier of the Segue

Let me show you how I usually implement the prepare(for:sender:) method of a view controller. I safely unwrap the identifier of the segue. Because I'm only interested in segues with an identifier, I use a guard statement. If a segue doesn't have an identifier, then it makes no sense to continue because I don't know which segue I'm dealing with.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let identifier = segue.identifier else { return }

    guard let indexPath = tableView.indexPathForSelectedRow else { return }

    // Fetch Note
    let note = notes[indexPath.row]

    guard let destination = segue.destination as? DetailViewController else { return }

    // Configure Detail View Controller
    destination.note = note
}

We switch on the value stored in the identifier constant and add a default case to make the switch statement exhaustive. Notice that I throw a fatal error if indexPathForSelectedRow returns nil and when the destination view controller cannot be cast to an instance of the DetailViewController class. Because neither of these scenarios should happen in production, I throw a fatal error.

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let identifier = segue.identifier else { return }

    switch identifier {
    case Segue.NoteDetails:
        guard let indexPath = tableView.indexPathForSelectedRow else {
            fatalError("No Note Selected")
        }

        guard let destination = segue.destination as? DetailViewController else {
            fatalError("Unexpected Destination View Controller")
        }

        // Fetch Note
        let note = notes[indexPath.row]

        // Configure Detail View Controller
        destination.note = note
    default:
        break
    }
}

That's it. Thanks to the Segue enum, we don't need to rely on a random string literal in the implementation of the prepare(for:sender:) method.

What About the Root View Controller

Another common question I hear about dependency injection is related to the initial view controller of a storyboard. The operating system loads the storyboard specified in the target's deployment details and instantiates its initial view controller.

The operating system loads the storyboard specified in the target's deployment details and instantiates its initial view controller.

This is nice as long as you don't need to inject any dependencies into the initial view controller. The solution isn't difficult, though. Let's take a look at the application delegate of the project.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    // MARK: - Properties

    var window: UIWindow?

    // MARK: -

    var notes: [Note] {
        return [
            Note(title: "Monday, January 11", contents: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ac dolor justo, ac tempus leo. Etiam pulvinar eros at lectus sollicitudin scelerisque."),
            Note(title: "Another Day", contents: "Aliquam erat volutpat. Suspendisse eu eros non elit blandit suscipit. Morbi scelerisque euismod tempus. Vestibulum elementum tincidunt tempor. Mauris sodales tristique adipiscing."),
            Note(title: "Ideas", contents: "Sed venenatis lorem quis eros hendrerit consequat. Sed a est leo. Donec sapien libero, rutrum eget luctus ac, accumsan vel lectus. Ut quis libero ante. Ut volutpat, massa ac aliquam molestie, neque est blandit diam, non adipiscing purus magna vitae massa."),
            Note(title: "Help", contents: "Vestibulum fermentum consectetur sem, non aliquet nisl varius porta. Nulla consectetur tellus vel nibh tincidunt nec tincidunt nunc pellentesque. Etiam vel arcu sit amet quam auctor tincidunt commodo eu leo. Aliquam in arcu nulla. Donec eget imperdiet dui. Praesent vitae odio leo. Morbi bibendum lobortis sapien sit amet posuere.")
        ]
    }

    // MARK: - Application Life Cycle

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Initialize Root View Controller
        guard let rootViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController() as? RootViewController else {
            fatalError("Unable to Instantiate Root View Controller")
        }

        // Configure Root View Controller
        rootViewController.notes = notes

        // Configure Window
        window?.rootViewController = rootViewController

        // Make Key and Visible
        window?.makeKeyAndVisible()

        return true
    }

}

The AppDelegate class defines a private, computed property, notes, that returns an array of Note instances. In production, you'd load the notes from a file on disk or fetch them from a remote server. We use the notes computed property for convenience.

We need to inject the array of notes into the RootViewController instance. Let's take a look at the implementation of application(_:didFinishLaunchingWithOptions:) to see how this works.

// MARK: - Application Life Cycle

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Initialize Root View Controller
    guard let rootViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController() as? RootViewController else {
        fatalError("Unable to Instantiate Root View Controller")
    }

    // Configure Root View Controller
    rootViewController.notes = notes

    // Configure Window
    window?.rootViewController = rootViewController

    // Make Key and Visible
    window?.makeKeyAndVisible()

    return true
}

We instantiate the initial view controller of the main storyboard and use a guard statement to safely cast the result of instantiateInitialViewController() to an instance of the RootViewController class.

// Initialize Root View Controller
guard let rootViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateInitialViewController() as? RootViewController else {
    fatalError("Unable to Instantiate Root View Controller")
}

With a reference to the root view controller, we can set its notes property.

// Configure Root View Controller
rootViewController.notes = notes

This technique only works if we set the rootViewController property of the application delegate's window property to the root view controller.

// Configure Window
window?.rootViewController = rootViewController

We invoke the makeKeyAndVisible() method of the window and return true from the method.

// Make Key and Visible
window?.makeKeyAndVisible()

return true

What's Next

Even though the example we discussed in this episode is simple, it doesn't get more complex than this. If you decide to use storyboards, then property injection is your only viable choice. You can also rely on a framework or library, such as Typhoon or Swinject, if you don't mind adding another dependency to your project. That's a choice you need to make.

Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By
Next Episode "Dependency Injection With XIB Files"