Earlier in this series, I showed you that dependency injection with storyboards isn't complicated once you understand how the various pieces fit together. We haven't covered tab bar controllers in this series and it seems quite a few developers run into problems when working with storyboards and tab bar controllers. It's a bit more complicated, but that complexity disappears once you understand how everything fits together.
Starter Project
For this episode, I created a small project that includes a tab bar controller with three child view controllers. The application shows a white view with three tabs. Let's take a look at the main storyboard.
It contains a tab bar controller as the initial view controller and three child view controllers. One of the child view controllers lives in a separate storyboard. This illustrates that the complexity of the storyboard is irrelevant to the strategy we're about to implement.
No Segues
Even though each child view controller is connected to the tab bar controller through a segue, the segue isn't going to help us configure the child view controllers. The prepare(for:sender:)
method isn't invoked when a child view controller is initialized and added as a child view controller to the tab bar controller.
We can verify this by implementing the prepare(for:sender:)
method in the TabBarController
class, a UITabBarController
subclass, and adding a breakpoint. If we run the application, the breakpoint we set isn't hit.
import UIKit
class TabBarController: UITabBarController {
// MARK: - Properties
var applicationManager: ApplicationManager?
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Overrides
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("\(#function), \(#line), \(#file)")
}
}
Child View Controllers
We can't take advantage of segues, but there's a simple solution. A tab bar controller keeps a reference to its child view controllers. We can access that array through the viewControllers
property of the tab bar controller. That's part of the solution. In the viewDidLoad()
method of the TabBarController
class, we invoke a helper method, setupChildViewControllers()
.
override func viewDidLoad() {
super.viewDidLoad()
// Setup Child View Controllers
setupChildViewControllers()
}
Let's find out what the implementation of the setupChildViewControllers()
method looks like. Because the viewControllers
property is of an optional type, we use a guard
statement to safely unwrap its value.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
}
We then iterate through the array of child view controllers and inject any dependencies they need.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
}
}
A switch
statement is a perfect fit for this scenario. We use value binding and pattern matching to access each of the view controllers. Let's start with the RedViewController
instance. We set the color
property of the RedViewController
instance to a red color.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
switch viewController {
case let viewController as RedViewController:
// Configure View Controller
viewController.color = UIColor.red
default:
break
}
}
}
Run the application. The view of the first child view controller should now have a red background color.
Navigation Controllers
The second child view controller requires a bit more configuration. We initialize an instance of the GreenViewModel
struct and assign it to the viewModel
property of the GreenViewController
instance.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
switch viewController {
case let viewController as RedViewController:
// Configure View Controller
viewController.color = UIColor.red
case let viewController as GreenViewController:
// Initialize View Model
let viewModel = GreenViewModel(title: "Green")
// Configure View Controller
viewController.viewModel = viewModel
default:
break
}
}
}
Let's take a look at the implementation of the GreenViewController
class before we run the application. In the viewDidLoad()
method, we invoke a helper method, setupViewModel(with:)
.
override func viewDidLoad() {
super.viewDidLoad()
// Setup View Model
setupViewModel(with: viewModel)
}
In setupViewModel(with:)
, we safely unwrap the value stored in the viewModel
parameter and use it to populate the title label.
import UIKit
class GreenViewController: UIViewController {
// MARK: - Properties
@IBOutlet var titleLabel: UILabel!
// MARK: -
var viewModel: GreenViewModel?
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Setup View Model
setupViewModel(with: viewModel)
}
// MARK: - Helper Methods
private func setupViewModel(with viewModel: GreenViewModel?) {
guard let viewModel = viewModel else {
return
}
// Configure Title Label
titleLabel.text = viewModel.title
}
}
Run the application to see the result. If we tap the middle tab, we should see a label displaying the word Green. That isn't the case, though. Something's not right.
Open Main.storyboard. The GreenViewController
instance is embedded in a navigation controller. It's the navigation controller that is a child view controller of the tab bar controller. We need to ask the navigation controller for its root view controller.
Open TabBarController.swift and revisit setupChildViewControllers()
. In the for
loop, we define a variable property, childViewController
, of type UIViewController?
. We use optional binding to cast viewController
to a UINavigationController
instance. If that fails, we assign viewController
to childViewController
. In that scenario, the view controller isn't embedded in a navigation controller.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
var childViewController: UIViewController?
if let navigationController = viewController as? UINavigationController {
} else {
childViewController = viewController
}
...
}
}
If the view controller is a UINavigationController
instance, we ask it for the first view controller of the navigation stack and assign that value to childViewController
.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
var childViewController: UIViewController?
if let navigationController = viewController as? UINavigationController {
childViewController = navigationController.viewControllers.first
} else {
childViewController = viewController
}
...
}
}
Last but not least, we update the switch
statement. We switch on the value stored in childViewController
instead of viewController
.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
var childViewController: UIViewController?
if let navigationController = viewController as? UINavigationController {
childViewController = navigationController.viewControllers.first
} else {
childViewController = viewController
}
switch childViewController {
case let viewController as RedViewController:
// Configure View Controller
viewController.color = UIColor.red
case let viewController as GreenViewController:
// Initialize View Model
let viewModel = GreenViewModel(title: "Green")
// Configure View Controller
viewController.viewModel = viewModel
default:
break
}
}
}
Run the application and verify that the label of the green view controller displays the word Green.
Passing Along Dependencies
Passing dependencies from one view controller to another is just as easy. The TabBarController
class defines a property, applicationManager
, of type ApplicationManager?
. Let's inject that dependency into the tab bar controller in the application delegate and pass it to the BlueViewController
instance, the third child view controller of the tab bar controller.
import UIKit
class TabBarController: UITabBarController {
// MARK: - Properties
var applicationManager: ApplicationManager?
...
}
Open AppDelegate.swift. In application(_:didFinishLaunchingWithOptions:)
, we ask the application window for its root view controller and cast it to an instance of the TabBarController
class. Because this operation should never fail, we wrap it in a guard
statement and throw a fatal error if the root view controller isn't a TabBarController
instance.
// MARK: - Application Life Cycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
guard let rootViewController = window?.rootViewController as? TabBarController else {
fatalError("Unexpected Root View Controller")
}
return true
}
We instantiate an instance of the ApplicationManager
class and assign it to the applicationManager
property of the TabBarController
instance.
// MARK: - Application Life Cycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
guard let rootViewController = window?.rootViewController as? TabBarController else {
fatalError("Unexpected Root View Controller")
}
// Configure Root View Controller
rootViewController.applicationManager = ApplicationManager()
return true
}
Passing the ApplicationManager
instance to the BlueViewController
instance is straightforward. In setupChildViewControllers()
, we add a case to the switch
statement for the blue view controller and assign the value of the applicationManager
property to the applicationManager
property of the blue view controller.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
var childViewController: UIViewController?
if let navigationController = viewController as? UINavigationController {
childViewController = navigationController.viewControllers.first
} else {
childViewController = viewController
}
switch childViewController {
case let viewController as RedViewController:
// Configure View Controller
viewController.color = UIColor.red
case let viewController as GreenViewController:
// Initialize View Model
let viewModel = GreenViewModel(title: "Green")
// Configure View Controller
viewController.viewModel = viewModel
case let viewController as BlueViewController:
// Configure View Controller
viewController.applicationManager = applicationManager
default:
break
}
}
}
If the application manager is successfully injected into the blue view controller, then the blue view controller should display the version number of the application in its view. The version number is provided by the application manager. The blue view controller asks the application manager for the version number and displays it in a label.
import UIKit
class BlueViewController: UIViewController {
// MARK: - Properties
@IBOutlet var versionLabel: UILabel!
// MARK: -
var applicationManager: ApplicationManager?
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Setup View
setupView()
}
// MARK: - View Methods
private func setupView() {
// Configure Version Label
versionLabel.text = applicationManager?.versionAsString
}
}
Run the application and verify that the blue view controller displays the version number of the application.
Tab Bar Items
Another common problem developers face when working with tab bar controllers is configuring the tab bar items of the tab bar. The tab bar items don't display anything meaningful at the moment.
This is easy to fix. Let's use the green view controller as an example. The title of the tab bar item is populated with the value of the view controller's title
property. Let's set the title
property of the GreenViewController
instance in its viewDidLoad()
method.
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = "Green"
// Setup View Model
setupViewModel(with: viewModel)
}
Run the application. Notice that the tab bar item doesn't display the view controller's title. How is that possible?
The viewDidLoad()
method of a child view controller is invoked when the child view controller is about to be presented. The child view controllers of a tab bar controller are initialized when the tab bar controller is initialized, but the view of a child view controller is loaded lazily. This improves performance.
The solution isn't difficult. We need to set the title
property of the child view controller in the setupChildViewControllers()
method.
// MARK: - Helper Methods
private func setupChildViewControllers() {
guard let viewControllers = viewControllers else {
return
}
for viewController in viewControllers {
var childViewController: UIViewController?
if let navigationController = viewController as? UINavigationController {
childViewController = navigationController.viewControllers.first
} else {
childViewController = viewController
}
switch childViewController {
case let viewController as RedViewController:
// Configure View Controller
viewController.title = "Red"
viewController.color = UIColor.red
case let viewController as GreenViewController:
// Initialize View Model
let viewModel = GreenViewModel(title: "Green")
// Configure View Controller
viewController.title = "Green"
viewController.viewModel = viewModel
case let viewController as BlueViewController:
// Configure View Controller
viewController.title = "Blue"
viewController.applicationManager = applicationManager
default:
break
}
}
}
Run the application. Each tab bar item now displays the title of the corresponding child view controller.
What's Next?
Dependency injection isn't as complex as some people make you believe. I hope this series has shown that injecting dependencies into view controllers isn't hard.