Dependency Injection With View Controllers

Dependency Injection With XIB Files

Dependency Injection With View Controllers
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

Many developers are a bit wary of storyboards and I can understand why that is. It's fine if you're not ready to embrace storyboards in your projects. Let me show you how to adopt dependency injection if you're using XIB files.

XIB Files

If you're not using storyboards and segues, then you are in charge of instantiating the view controllers of your project. While this requires a bit of additional work from your part, it has a few benefits. Because you are responsible for the initialization of each view controller, you can use initializer injection to inject the view controller's dependencies.

The example we explore in this episode is similar to the example of the previous episode. Download the starter project of this episode if you'd like to follow along. Notice that the main storyboard is missing. Each view controller of the project has a XIB file that defines its user interface.

The approach I recommend involves two steps. First, we define a designated initializer that accepts the dependencies of the view controller. Second, we set the view controller's dependencies in the initializer and invoke the designated initializer of its superclass.

Root View Controller

Let's apply this technique to the RootViewController class. Before we implement the initializer, we declare the notes property as a constant and don't assign it an initial value. This implies that the value of the notes property needs to be set during initialization and that it cannot be modified once it's set. That is one of the advantages of using initializer injection.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

    // MARK: -

    let notes: [Note]

    ...

}

Implementing a Designated Initializer

The compiler throws an error. We can fix the error by defining a designated initializer for the RootViewController class that accepts an array of Note instances.

// MARK: - Initialization

init(with notes: [Note]) {
    // Set Notes
    self.notes = notes

    super.init(nibName: "RootViewController", bundle: Bundle.main)
}

We assign the value of the notes parameter to the notes property. Once every stored property has a valid value, we can invoke the designated initializer of the superclass, init(nibName:bundle:).

The compiler doesn't agree with the changes we made. It throws another error. It notifies us that we need to implement a required initializer of the UIViewController class, init(coder:).

We need to implement a required initializer.

If we click the error, Xcode offers a solution. Click the "Fix" button to see what solution Xcode has in store for us.

Xcode offers a solution.

This is what the implementation of the init(coder:) initializer looks like.

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

Because we defined a designated initializer, we are required to override the required initializers of the superclass, that is, the UIViewController class.

You may be wondering why Xcode adds an implementation of the init(coder:) method in which a fatal error is thrown. That sounds like asking for trouble.

To understand why Xcode offers this solution, we need to modify the implementation. Let's remove the fatal error and invoke the implementation of the superclass. This introduces another problem, though.

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

While this may appear to be a viable solution, it isn't. The compiler notifies us that the notes property doesn't have a valid value before we invoke the init(coder:) method of the superclass.

The compiler continues throwing errors.

How do we set the value of the notes property? We can't pass a value for the notes property to the initializer because we need to implement the designated initializer as it's defined by the superclass. The answer is "We don't."

We won't be using the init(coder:) method and, if we want to use initializer injection, the only option we have is throwing a fatal error in the initializer. You may have seen this implementation before and wondered why a fatal error is thrown. This is the reason.

required init?(coder aDecoder: NSCoder) {
    fatalError("You should not instantiate this view controller by invoking init(coder:).")
}

But it's the message of the fatalError(_:file:line:) function that confuses many developers. A better message would be "You should not instantiate this view controller by invoking init(coder:)." That makes more sense and explains why a fatal error is thrown.

Because the fatalError(_:file:line:) function doesn't return control to the initializer, the compiler is happy with this implementation. But remember that, if the init(coder:) method is invoked, the application is terminated. That's why it should never be invoked.

Invoking the Designated Initializer

It's time to put the designated initializer to use. Open AppDelegate.swift and create an instance of the RootViewController class by invoking init(with:), passing in the array of note instances.

// MARK: - Application Life Cycle

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Initialize Root View Controller
    let rootViewController = RootViewController(with: notes)

    // Initialize Window
    window = UIWindow(frame: UIScreen.main.bounds)

    // Configure Window
    window?.rootViewController = rootViewController

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

    return true
}

Because we pass the array of Note instances as an argument of the initializer, we don't need to configure the root view controller. We set the root view controller as the root view controller of the application's window.

Notice that we explicitly initialize the window of the application in application(_:didFinishLaunchingWithOptions:). Because we're not using a storyboard, we are responsible for initializing and configuring the window of the application.

Benefits and Drawbacks

Even though I like the conveniences storyboards offer, I prefer this implementation in the context of dependency injection. Why is that? As I mentioned earlier, we are responsible for instantiating the view controller. That means we can implement a custom initializer and pass the dependencies of the view controller during initialization. The result is that the dependencies can be declared as constants.

But there's another subtle benefit. The dependencies are defined by the initializer. The interface of the RootViewController class defines the dependencies of the view controller. Any developer inspecting the custom initializer of the RootViewController class understands that the class needs an array of Note instances to do its work.

Detail View Controller

The changes we need to make to the DetailViewController class are similar. We start by declaring the note property as a constant property. It no longer needs to be an optional, which means we don't need to rely on optional chaining in the viewDidLoad() method.

import UIKit

class DetailViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var contentsLabel: UILabel!

    // MARK: -

    let note: Note

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Configure Labels
        titleLabel.text = note.title
        contentsLabel.text = note.contents
    }

    // MARK: - Actions

    @IBAction func done(_ sender: Any) {
        // Dismiss View Controller
        dismiss(animated: true)
    }

}

To instantiate the detail view controller, we need to invoke init(nibName:bundle:), passing in the name of the XIB file and the bundle in which the XIB file lives. We need to set the value of the note property before we do this. Remember that every stored property needs to have a valid value before the end of the initialization and before we invoke the initializer of the superclass.

// MARK: - Initialization

init(with note: Note) {
    // Set Note
    self.note = note

    super.init(nibName: "DetailViewController", bundle: Bundle.main)
}

Because we implemented a custom initializer, we are required to override the required initializer of the UIViewController class. We need to implement the init(coder:) method. Its implementation is identical to that of the RootViewController class.

required init?(coder aDecoder: NSCoder) {
    fatalError("You should not instantiate this view controller by invoking init(coder:).")
}

Because we're not using segues to navigate the application, we don't need to implement the prepare(for:sender:) method in the RootViewController class. We need to present the detail view controller manually when the user taps a row in the table view. We do this in the tableView(_:didSelectRowAt:) method of the UITableViewDelegate protocol.

extension RootViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    }

}

We safely unwrap the value that's returned by indexPathForSelectedRow and use the index path to fetch the Note instance that corresponds with the row the user tapped. We initialize an instance of the DetailViewController class and pass in the Note instance as an argument. We present the detail view controller modally by invoking present(_:animated:completion:).

extension RootViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let indexPath = tableView.indexPathForSelectedRow else { return }

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

        // Initialize Detail View Controller
        let detailViewController = DetailViewController(with: note)

        // Present Detail View Controller
        present(detailViewController, animated: true)
    }

}

Build and run the application to see if everything works as expected.

Keep It Private

There's another benefit of using initializer injection over property injection that I'd like to point out. Because we pass the dependencies of the RootViewController and DetailViewController classes during initialization, we can declare the dependencies of these classes private.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

    // MARK: -

    private let notes: [Note]

    ...

}
import UIKit

class DetailViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var contentsLabel: UILabel!

    // MARK: -

    private let note: Note

    ...

}

Why is this important? A key benefit Swift has over Objective-C is powerful access control. It pays to embrace access control in Swift. Let me show you what I mean.

Open RootViewController.swift and choose Jump to Generated Interface from Xcode's Navigate menu. Xcode shows us the interface of the RootViewController class. This is similar to a header file in Objective-C.

import UIKit

internal class RootViewController : UIViewController {

    @IBOutlet internal var tableView: UITableView!

    internal init(with notes: [Note])

    required internal init?(coder aDecoder: NSCoder)

    override internal func viewDidLoad()

    override internal func viewDidAppear(_ animated: Bool)
}

extension RootViewController : UITableViewDataSource {

    internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int

    internal func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
}

extension RootViewController : UITableViewDelegate {

    internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)

    internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
}

Notice that there's no trace of the notes property since we declared it private. A developer new to the project sees that the initializer accepts an array of Note instances, but they don't know anything about the internals of the RootViewController class.

And that's how it should be. Other objects don't need to know how the RootViewController class manages the array of Note instances it's given during initialization. Ignorance is bliss and this very often applies to software development.

What's Next

This episode illustrates that initializer injection has several benefits property injection misses. Unfortunately, you can only adopt initializer injection for view controllers if you choose to use XIB files or, as we'll see in the next episode, view controllers without XIB files or storyboards.

Initializer injection has the advantage that dependencies can be declared as constants and private. While these benefits are subtle, they are profound if your goal is to write clean, elegant, and robust software.

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
Next Episode "Dependency Injection In Code"