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:)
.
If we click the error, Xcode offers a solution. Click the "Fix" button to see what solution Xcode has in store for us.
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.
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.