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.
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.