Container view controllers manage the content from multiple view controllers, and each have their own approach to view hierarchies. Certain container view controllers that you may encounter include
Tab Bar Controllers display icons at the bottom of the screen that represent different View Controllers. Tool Bar displays icons that represent different commands to manipulate data currently displayed on the screen.
Like a Navigation Controller, a Tab Bar Controller also groups related View Controllers together. The main difference is that a Navigation Controller can only display View Controllers in sequential order. On the other hand, a Tab Bar Controller can jump to another View Controller directly.
While a Navigation Controller always displays a navigation bar and a Back button on every View Controller it opens, a Tab Bar Controller always displays a list of icons at the bottom of the screen where each icon represents a different View Controller. By tapping on an icon, the user can jump to that particular View Controller right away. Then the user can return to the Tab Bar Controller again to see the list of icons representing the other View Controllers.
Switching View Controllers in a Tab Bar Controller
Tab Bar Controllers make it easy to switch from one view to another in any order you wish. Each icon on the Tab bar represents a different View Controller. The basic steps to using a Tab Bar Controller to represent different views are to add multiple View Controllers to your storyboard and then Ctrl-drag to connect the Tab Bar Controller to a View Controller.
Then the next step is to create a title and icon to represent that View Controller. You may also want to reorder the icons on the Tab bar or delete a View Controller and its Tab bar icon later.
To see how to create a simple Tab Bar Controller, follow these steps:
Sharing data between tabs
Notice that if you are one tab and then go to the other tab, your data seem to have disappeared. Several alternative solutions exist for sharing data between tabs, each with their own pros and cons:
Global variables. If you create a variable outside of a class or struct, it’s automatically defined in the global scope, available from anywhere in your project. You could, for example, create a GlobalVars.swift file, that contains
var moviesManager = MoviesManager()
The movies view controllers would then automatically reference the moviesManager
property in the global scope.
When the user navigates away from a tab, the related view controller will remain in memory. If the data is edited while the user is on a different tab, this change in data won’t be represented when the user returns. To ensure the table or collection is up to date when the user returns, you'll probably want to request them to reload in the viewDidAppear
method.
There are several disadvantages of global variables: they create possible conflicts, make unit testing difficult, and make code more difficult to understand and harder to maintain.
Singletons. Singletons enforce that an object has been instantiated once and only once by main- taining its own instance internally.
It’s straightforward to convert a class to a singleton in Swift. All a class requires is a type property containing a reference to an instance of the class. You could make the singleton’s initializer private to ensure it can only be referenced from this instance property and not reinstantiated.
The following listing demonstrates how to convert MoviesManager
to a singleton.
class MoviesManager { static let instance = MoviesManager() private init() {} lazy var movies: [Movie] = self.loadMovies() }
Inside your ViewController classes no longer instantiate a MoviesManager
; rather, they access it via the instance
property:
var moviesManager = MoviesManager.instance
Like the global variables solution, in this solution, view controllers won’t receive notification when the user updates data in another tab, and you'll want to reload the table or collection views in the viewDidAppear
method.
There are several disadvantages of singletons: connections between different parts of your app can be obscured, making your app difficult to maintain and test.
Dependency injection. With dependency injection, an object can inject a dependency such as data or a service into another object.
Let’s use dependency injection to inject the moviesManager
into the two scenes. Because the tab bar controller controls both scenes, it makes sense to instantiate the moviesManager
in a subclass of the UITabBarController
and then inject it into the scenes when required.
Remove the instantiation of the movies manager in the FirstViewController
and SecondViewController
classes, and replace with an implicitly unwrapped optional:
var moviesManager = MoviesManager!
Create a class that subclasses UITabBarController
, and call it TabBarController
.
Set TabBarController
as the class of the tab bar controller in the Identity Inspector in the storyboard.
Instantiate the movies manager in your new class:
var moviesManager = MoviesManager()
To inject the movies manager into both view controllers, you’ll set up an Injectable
protocol that both view controllers will adopt. Create the Injectable
protocol in the TabBarController.swift file, with an inject
method ready to pass in the movies manager.
protocol Injectable { func inject(data: MoviesManager) }
In the table view controller, adopt the Injectable
protocol and implement the inject
method:
class FirstViewController: UITableViewController, Injectable { func inject(data: MoviesManager) { self.moviesManager = data } }
After setting the instance variable, this is a good time to reload the table view. Use optional binding in case the tableView
implicitly unwrapped optional hasn’t yet been instantiated.
Ensure the table view is up to date whenever the view appears:
override func viewDidAppear(_ animated: Bool) { tableView?.reloadData() }
Repeat steps from above again, but for the other view controller. The view controllers are now ready to be injected!
Back in the TabBarController
class, you’ll inject both view controllers with the data in the viewDidLoad
method. UITabBarController
has a viewControllers
array that stores a reference to the navigation controllers containing the view controllers for each tab.
Loop through the viewControllers
array, getting a reference to the navigation controllers, from which you can then get a reference to its root view controller to inject your data!
for navController in viewControllers! { if let navController = navController as? UINavigationController, let viewController = navController.viewControllers.first as? Injectable { viewController.inject(data: moviesManager) } }
Customizing Tab Bar Icons
Tab Bar Controllers display icons on a Tab bar at the bottom of the screen. By default, this icon simply displays a generic label such as "Item", but you can customize this icon to include more descriptive text and/or an icon.
To see how to customize Tab bar icons, follow these steps: