Dependency Injection With View Controllers

Dependency Injection In Code

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

In the early days of the iPhone, many developers shied away from Interface Builder for creating user interfaces. Even though Xcode had been around for years and years, if you wanted your application to be performant, you created your user interfaces in code. The first iPhone wasn't that powerful and you had to squeeze every ounce of performance from it by optimizing how its resources were used.

This is no longer true and few developers avoid Interface Builder. Interface Builder has become more important for a number of reasons. Creating applications that look great on every device your application can run on becomes easier if you take advantage of Auto Layout and Interface Builder.

No XIBs and No Storyboards

Even though I almost always use XIB files and storyboards, I'd like to show you how to use dependency injection with view controllers if you create the user interface of your applications in code. You may be surprised by what you learn in this episode.

We start where we left off in the previous episode. Download the starter project of this episode if you'd like to follow along.

Swift Is Different

Swift is very different from Objective-C in many ways and this episode highlights one of these differences. Let's find out what it takes to instantiate the RootViewController instance we create in the AppDelegate class without a XIB file or a storyboard.

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
}

The custom initializer we invoke in application(_:didFinishLaunchingWithOptions:) is a good start. There's no mention of a XIB file or a storyboard. We need to make some changes to the custom initializer since we no longer intend to use a XIB file to create the user interface of the view controller.

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

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

Since we're no longer using a XIB file, we can't invoke init(nibName:bundle:). Let's see what happens if we comment out this line in the custom initializer.

The compiler informs us that we are required to invoke a designated initializer of the UIViewController class.

Even though the error of the compiler may sound cryptic, it highlights a key aspect of classes in Swift. The compiler reminds us that we are required to invoke a designated initializer of the UIViewController class, the superclass of the RootViewController class, before returning from the custom initializer we defined. That's a bit of a problem.

Because we decided not to use XIB files or storyboards, we can't invoke init(nibName:bundle:). The only other option we have is invoking init(coder:). But remember that we throw a fatal error whenever init(coder:) is invoked.

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

We could decide not to throw a fatal error in init(coder:) and invoke init(coder:) in the custom initializer. This introduces a slew of other problems, though.

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

    super.init(coder: NSCoder())
}

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

First, init(coder:) is a failable initializer. It returns an optional while init(with:) doesn't. While we could resolve this issue with the fatalError(_:file:line:) function, that's a workaround I'm not happy with.

Second, we need an NSCoder instance to invoke init(coder:). The only option we have is creating one by invoking the init() method of the NSCoder class.

Third, the implementation of the init(coder:) method is incomplete. It's not assigning a value to the notes property. Remember that every stored property needs to have a valid value before the initialization of the instance is completed.

It seems we've coded ourselves in a corner. This doesn't mean we can't use dependency injection if we create the view controllers of the project in code. However, it does mean that we cannot rely on initializer injection to inject the dependencies into the view controllers of the project.

Refactoring the Root View Controller

Let's start with the RootViewController class. Because we can't inject the array of Note instances into the view controller during initialization, we need to rely on property injection. This implies we have to declare the notes property as a variable and assign an empty array as its initial value. This also means we can no longer declare the property private.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

    // MARK: -

    var notes: [Note] = []

    ...

}

We remove the custom initializer of the RootViewController class to satisfy the compiler.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var tableView: UITableView!

    // MARK: -

    var notes: [Note] = []

    // MARK: - View Life Cycle

    override func viewDidLoad() { ... }

    override func viewDidAppear(_ animated: Bool) { ... }

}

To instantiate a RootViewController instance in the AppDelegate class, we invoke the init() method of the RootViewController class. The NSObject class is part of the inheritance tree of the UIViewController class, which means we have access to the init() initializer defined by the NSObject class.

open class NSObject : NSObjectProtocol {

    ...

    public init()

    ...

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

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

    // Configure Window
    window?.rootViewController = rootViewController

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

    return true
}

Because we're no longer passing the array of Note instances to the initializer, we need to use property injection to inject the array of Note instances into the RootViewController instance. This is similar to the approach we adopted in the first episode of this series when we used storyboards.

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

    // Configure Root View Controller
    rootViewController.notes = notes

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

    // Configure Window
    window?.rootViewController = rootViewController

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

    return true
}

We can't build and run the application yet since the RootViewController class hasn't been updated to create its own user interface. Let's first focus on the refactoring of the DetailViewController class.

Refactoring the Detail View Controller

The changes we need to make are similar. We need to declare the note property as a variable. It can no longer be declared private and it needs to be an optional. Why is that?

import UIKit

class DetailViewController: UIViewController {

    // MARK: - Properties

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

    // MARK: -

    var note: Note?

    ...

}

Every stored property needs to have a valid value before the initialization of the instance is completed. We won't be passing the Note instance to the initializer. That won't work. But the note property still needs to have a valid value before the initialization of the instance is completed.

The only option is to declare it as an optional. An optional has a default value of nil. This isn't perfect since we'd rather not use an optional, but it's the safest option we have. We could declare the note property as an implicitly unwrapped optional. That's a riskier option, but it's a viable solution.

Turning the note property into an optional means we need to use optional chaining in the viewDidLoad() method.

override func viewDidLoad() {
    super.viewDidLoad()

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

We get rid of the custom initializer of the DetailViewController class as well as the init(coder:) implementation.

import UIKit

class DetailViewController: UIViewController {

    // MARK: - Properties

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

    // MARK: -

    var note: Note?

    // MARK: - View Life Cycle

    override func viewDidLoad() { ... }

    // MARK: - Actions

    @IBAction func done(_ sender: Any) { ... }

}

It's time to update the tableView(_:didSelectRowAt:) method of the RootViewController class. We initialize the DetailViewController instance by invoking init() and inject the Note instance that corresponds with the user's selection using property injection.

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()

    // Configure Detail View Controller
    detailViewController.note = note

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

Creating the User Interface

The changes we made are minor, but I hope you understand why we were forced to make these changes. That's the most important takeaway of this episode.

Before we can build and run the application, we need to create the user interface of the RootViewController and DetailViewController classes. I won't cover this in this episode. Feel free to browse the finished project of this episode if you'd like to see the result.

What's Next

Using view controllers without XIB files or storyboards may seem appealing if you're having a rough day working with Interface Builder. I hope this episode has shown you that it isn't that appealing of an option. Whenever you initialize an instance of a class, the compiler forces you to stick to a strict set of rules. Much of the flexibility that developers relied on in Objective-C is absent in Swift.

This series should have given you a better idea of dependency injection with view controllers. But I hope that it has also illustrated that storyboards aren't as bad as they're often portrayed. I default to storyboards and only rely on XIB files if there's no other option. That strategy has worked very well for me.

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 With Tab Bar Controllers"