There's a common scenario we haven't covered in this series. How do you use a tab bar controller in combination with coordinators? The coordinator pattern is a flexible design pattern and there are several options for using a tab bar controller in combination with coordinators.
A few of the options require a UITabBarController subclass. That's something I want to avoid, though. The UITabBarController class offers enough flexibility to avoid subclassing. In this episode, I share the solution I have in mind.
Exploring the Starter Project
For this episode, I have added a new UIViewController subclass to the project, the ProfileViewController class. Open ProfileViewController.swift in View Controllers > Profile View Controller. The ProfileViewController class conforms to the Storyboardable protocol. The user interface of the profile view controller is defined in Profile.storyboard. That is why the ProfileViewController class implements the storyboardName static property of the Storyboardable protocol. It returns the name of the storyboard.
The implementation of the ProfileViewController class is short. It defines an outlet for a label, titleLabel. In a didSet property observer, the view controller sets the text property of the title label. We also implement the init(coder:) initializer, a required initializer. In the initializer, we set the title property of the view controller.
import UIKit
class ProfileViewController: UIViewController, Storyboardable {
// MARK: - Storyboardable
static var storyboardName: String {
return "Profile"
}
// MARK: - Properties
@IBOutlet var titleLabel: UILabel! {
didSet {
// Configure Title Label
titleLabel.text = title
}
}
// MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Set Title
title = "Profile"
}
}
Open Profile.storyboard. The storyboard contains one view controller scene. Select the Profile View Controller scene and open the Identity Inspector on the right. Class and Storyboard ID are both set to ProfileViewController. The storyboard identifier is used by the Storyboardable protocol to instantiate the view controller.

Creating a Plan of Action
The goal of this episode is straightforward. At the moment, the root view controller of the application window is the photos view controller. That needs to change. We replace the photos view controller with a tab bar controller. The tab bar controller will manage two tabs. The first tab shows the photos view controller. The second tab shows the profile view controller.
Remember that the coordinators of a project are responsible for creating and managing the view controllers. This means that one of the coordinators of the project needs to instantiate the tab bar controller.
A tab bar controller manages one or more view controllers. Each view controller corresponds with a tab in the tab bar. The tab bar controller keeps a reference to these view controllers in its viewControllers property, an array of UIViewController instances.
You may be starting to see how the coordinator pattern is going to fit into the story. Each tab will be managed by a separate coordinator. That coordinator will supply the view controller for the tab. By having a dedicated coordinator for each tab, the application automatically becomes modular and flexible. It's trivial to add, remove, or replace a coordinator, even at runtime. Let's get to work.
Renaming the Application Coordinator
The AppCoordinator class should no longer be responsible for bootstrapping the application. To make that clear, we change the name of the class to PhotosCoordinator. That makes more sense. Move AppCoordinator.swift to the Child Coordinators group and change the name of the file to PhotosCoordinator.swift. Open AppCoordinator.swift, right-click the class name, and choose Refactor > Rename... from the contextual menu. Change the name of the class to PhotosCoordinator and click Rename.
import UIKit
import Foundation
class PhotosCoordinator: Coordinator {
...
}
Creating the Profile Coordinator
We also need to create a coordinator for the profile view controller. Create a new Swift file in the Child Coordinators group and name it ProfileCoordinator.swift. Add an import statement for the UIKit framework and define a Coordinator subclass with name ProfileCoordinator.
import UIKit
class ProfileCoordinator: Coordinator {
}
The implementation of the ProfileCoordinator class is short and simple. We start by defining a private, constant property, profileViewController. We create a ProfileViewController instance and store a reference in the profileViewController property.
import UIKit
class ProfileCoordinator: Coordinator {
// MARK: - Properties
private let profileViewController = ProfileViewController.instantiate()
}
We declare the profileViewController property privately because no other objects should have direct access to the ProfileViewController instance. They don't need to know what type of view controller the profile coordinator manages.
Later in this episode, another coordinator needs access to the ProfileViewController instance, but it doesn't need to know about the exact type of the view controller. It needs a reference to a UIViewController instance. To make that possible, we declare a computed property, rootViewController, of type UIViewController. The computed property returns a reference to the ProfileViewController instance.
You may be wondering why we overcomplicate the implementation of the ProfileCoordinator class. Declaring the profileViewController property privately isn't strictly necessary, but it hides the implementation of the ProfileCoordinator class. We can easily change the implementation of the class without other objects knowing about it. You should always expose as few implementation details as possible. You can always share more if necessary.
import UIKit
class ProfileCoordinator: Coordinator {
// MARK: - Properties
var rootViewController: UIViewController {
return profileViewController
}
// MARK: -
private let profileViewController = ProfileViewController.instantiate()
}
Creating the Application Coordinator
Create a new Swift file in the Application Coordinator group and name it AppCoordinator.swift. Add an import statement for the UIKit framework and define a Coordinator subclass with name AppCoordinator.
import UIKit
class AppCoordinator: Coordinator {
}
We define a private, constant property, tabBarController. We create a UITabBarController instance and store a reference in the tabBarController property. We define the tabBarController property privately because no other objects need direct access to the UITabBarController instance.
import UIKit
class AppCoordinator: Coordinator {
// MARK: - Properties
private let tabBarController = UITabBarController()
}
Remember that the application coordinator provides the root view controller of the application window. The root view controller can be any UIViewController instance. We define a computed property, rootViewController, of type UIViewController. The rootViewController property returns a reference to the UITabBarController instance.
import UIKit
class AppCoordinator: Coordinator {
// MARK: - Properties
var rootViewController: UIViewController {
return tabBarController
}
private let tabBarController = UITabBarController()
}
Open AppDelegate.swift. We need to make one change. The appCoordinator property no longer holds a reference to a PhotosCoordinator instance. We instantiate an instance of the AppCoordinator class we created a moment ago and store a reference to the AppCoordinator instance in the appCoordinator property. That's the only change we need to make.
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
var window: UIWindow?
// MARK: -
private let appCoordinator = AppCoordinator()
...
}
Build and run the application to see the result. The application shows a black screen with a gray view at the bottom. It doesn't look pretty, but it's exactly what we should expect at this point. The gray view at the bottom is the tab bar of the tab bar controller. This means that we successfully set the tab bar controller of the application coordinator as the root view controller of the application window. We see a black screen because the tab bar controller doesn't have any view controllers to display.

Click the Debug View Hierarchy button in the debug bar at the bottom to bring up Xcode's view debugger. The view hierarchy in the Debug Navigator confirms that the root view controller of the application window is a tab bar controller and that the gray view at the bottom is the tab bar controller's tab bar.
Populating the Tab Bar Controller
The last step we need to take is populating the tab bar controller, that is, setting its viewControllers property. Stop the application, open AppCoordinator.swift, and override the initializer of the Coordinator class. We first invoke the initializer of the superclass.
import UIKit
class AppCoordinator: Coordinator {
// MARK: - Properties
var rootViewController: UIViewController {
return tabBarController
}
private let tabBarController = UITabBarController()
// MARK: - Initialization
override init() {
super.init()
}
}
The first tab of the tab bar controller is managed by the PhotosCoordinator class. We create an instance of the PhotosCoordinator class and store a reference in a constant with name photosCoordinator. The second tab of the tab bar controller is managed by the ProfileCoordinator class. We create an instance of the ProfileCoordinator class and store a reference in a constant with name profileCoordinator.
// MARK: - Initialization
override init() {
super.init()
// Initialize Child Coordinators
let photosCoordinator = PhotosCoordinator()
let profileCoordinator = ProfileCoordinator()
}
We pass the root view controllers of the photos and profile coordinators to the tab bar controller by updating its viewControllers property.
// MARK: - Initialization
override init() {
super.init()
// Initialize Child Coordinators
let photosCoordinator = PhotosCoordinator()
let profileCoordinator = ProfileCoordinator()
// Update View Controllers
tabBarController.viewControllers = [
photosCoordinator.rootViewController,
profileCoordinator.rootViewController
]
}
Remember from earlier in this series that the parent coordinator needs to keep a reference to the child coordinators it manages to prevent them from being deallocated. We append the photos and profile coordinators to the array of child coordinators of the application coordinator.
// MARK: - Initialization
override init() {
super.init()
// Initialize Child Coordinators
let photosCoordinator = PhotosCoordinator()
let profileCoordinator = ProfileCoordinator()
// Update View Controllers
tabBarController.viewControllers = [
photosCoordinator.rootViewController,
profileCoordinator.rootViewController
]
// Append to Child Coordinators
childCoordinators.append(photosCoordinator)
childCoordinators.append(profileCoordinator)
}
We're almost finished. Each child coordinator needs to be started. This is easy to do in the start() method of the AppCoordinator class. We iterate through the array of child coordinators and invoke the start() method on each child coordinator.
// MARK: - Overrides
override func start() {
childCoordinators.forEach { (childCoordinator) in
// Start Child Coordinator
childCoordinator.start()
}
}
Build and run the application one more time to see the result of the changes we made. This looks much better. The application shows a tab bar with two tabs, Photos and Profile. The Photos tab shows the photos view controller and the Profile tab shows the profile view controller.

What's Next?
This episode proves that the coordinator pattern works fine with tab bar controllers. It's important to note that we didn't need to modify the Coordinator class and that we used the UITabBarController class as is. The solution we implemented didn't require a UITabBarController subclass. This episode underlines the versatility and flexibility of the coordinator pattern.