Imagining Dependency Injection via Initializer with Storyboards

Storyboards, both their good and bad parts, are something I already analyzed in the past. To recap, they just don’t feel like the first class citizen in the Swift world. Third party tools can help but only with some of the issues, e.g. SwiftGen allows us to minimize the usage of “stringly typed” APIs.

At the end of the day though storyboards can be completely fixed only by Apple. So, let’s see how Apple could improve the biggest pain point there is – the lack of dependency injection via initializer.

Why Dependency Injection via Initializer Is Important

Dependency Injection (or passing stuff between objects) is a commonly used design pattern. There are two popular ways of injecting dependencies in Swift:

  • Initializer Injection – dependencies are passed to an initializer. Properties using them can be declared as non-optional constants:
    • let dependency: SomeType
  • Setter Injection – dependencies are provided after the initialization time. Properties storing them have to be variables, either optional or with an implicitly unwrapped optional attribute:
    • var dependency: SomeType? or
    • var dependency: SomeType!

The biggest advantage of the initializer injection is that it’s possible to confirm at the compile-time that all dependencies are set up properly. No need to run the app or tests to check that all the assumptions are met. This turns out to be a huge benefit in a large codebase!

All is good except that we can’t use initializer injection with storyboards. A view controller with a view defined in a storyboard is initialized this way:

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "detailsViewController")
present(viewController, animated: true, completion: nil)

We don’t call a view controller’s initializer directly, its instance is created for us by a UIStoryboard object. We can only pass data to it after the initialization time.

Idea

This problem has been bugging me for a long time. I recently came up to the conclusion that it can be divided into two cases:

  1. View controllers that are the destination of at least one segue
  2. View controllers being initialized only through instantiateViewController(withIdentifier:) and instantiateInitialViewController()

There’s not much that can be done about the first case. View controllers can be initialized and presented without any of our code being called, as it’s not mandatory to do anything in or even implement at all prepare(for segue: UIStoryboardSegue, sender: Any?) method.

So, let’s focus on the second case – the view controller initialization from code. What happens, in that case, is UIKit simply calling init(coder:) on our view controller passing an NSCoder instance to it. Seems like a dead end too. We can’t pass any instances along that path anyway, right?

Stack trace of the initialization triggered by instantiateViewController(withIdentifier:)

Well, what if we could reverse things a bit. Instead of letting a UIStoryboard instance initialize a view controller for us, we could get an NSCoder instance from it. So, we would have something like:

// Hypothetical method, not present in UIKit. `decoder` has the type `NSCoder`.
let decoder = storyboard.decoder(forViewControllerIdentifier: "detailsViewController")

Then, in our view controller we could have a new initializer:


class DetailsViewController: UIViewController {

    let dependency: SomeType

    init?(coder aDecoder: NSCoder, dependency: SomeType) {
        self.dependency = dependency
        super.init(coder: aDecoder)
    }
}

and initialize our view controller by calling that initializer directly (!)

let detailsViewController = DetailsViewController(coder: decoder, dependency: dependency)

That one initializer is not enough to satisfy the compiler, though. We would have to implement init(coder:) too. If we simply called super.init(coder: aDecoder) from within it, the compiler would now note that:

Property 'self.dependency' not initialized at super.init call

If we were sure that our view controller will not be used with segues, we could silence the error by putting fatalError() in that initializer. Otherwise, we would have to set the value of the dependency property without it being passed from the outside. That’s not always possible, though.

If we were to tie the code with storyboards closer, we could introduce a protocol for view controllers that can’t be initialized through segues, e.g. NotSegueInitializable.

Conclusion

We presented a simple addition to UIStoryboard class that could improve an important downside of that API. We weren’t able to confirm – not having access to UIKit’s source code – that this addition is indeed as simple as it seems.

With that API and with avoidance of segues we would have storyboards that don’t exercise most of the issues pointed out in “Storyboards and Their (Better) Alternatives.” Let’s hope that this is something Apple is currently working on and that we’ll see better APIs for storyboards soon.