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.

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.

Red View Controller

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.

Main Storyboard

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.

Green View Controller

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.

Blue View Controller

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.

Tab Bar Items

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?

Tab Bar Items

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.

Tab Bar Items

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.