Tabbed Navigations is pretty convenient and fun in iOS. Thanks to the native UITabBarController, a subclass of UIViewController, it doesn’t really need not much setting up other than its child ViewControllers.
But after that, admittedly, we do still find ourselves in between some dilemmas managing the child ViewControllers especially when we need access to them from the TabBarController. There are several different ways of accomplishing it depending on several more factors like weight or intention of the tasks, some of them being:
One simple way of tracking the child ViewControllers is through their String tabBarItem.title
property. If name comparison is all you need, maybe that will be do?
But be careful still. Strings are easy to implement and modify, but also very loose if you don’t pay strict attention to them. Using Strings as a point of comparison can easily result in a lot of false flags without you knowing, so I would stay away from it for now.
Another way can be done through data types. Similar to the title comparisons, you will probably end up with a lot of if else
‘s and because of this, it is only mostly applicable given no two child ViewControllers share the same type.
Unlike String comparisons, this may save you from accidentally misinterpreting rogue values. But if you go this route, testing the parent UITabBarController will be extra tricky since since relying on strict data types for identifying objects will limit your mocking capabilities.
Another way of managing child View Controllers that I’d like to share with you, is through a mix of Swift Enum and Protocol Implementation. By creating a finite set of TabBar identity values for your child ViewControllers to hold, you also improve their testability by giving them a good base for mock or test classes.
First, We’ll implement our finite set of identities namedTabBarType
through a set of Enum values representing our expected TabBar Items.
enum TabBarType: String {
case featured = "Featured"
case accounts = "Accounts"
case cart = "Cart"
case search = "Search"
case none = "None"
}
This way we can lock down our possible values to make sure we can catch any rogues as early as compile time.
Second, we’ll create a short Protocol named TabBarChild
to describe an object that holds a TabBarType
value representing them.
protocol TabBarChild {
var tabBarType: TabBarType? { get set }
}
Third, we’ll make our designated child ViewController classes conform to the new protocol so their parents know how to communicate to them as TabBar children. This can be accomplished in many different ways depending on your team’s preferred design, but in this article I can show you a couple.
The simpler one is through extending the UIViewController class to conform to the TabBarChild
protocol. You basically just assume every UIViewController to be TabBarChild
-capable with the drawback of having to design ViewControllers to store/compute the value of TabBarType
a little less conventionally.
extension UIViewController: TabBarChild {
var tabBarType: TabBarType {
get { return TabBarType(rawValue: tabBarItem.title ?? "" ) ?? .none }
set { tabBarItem.title = newValue.rawValue }
}
}
In this example, I chose to store its value to the ViewController’s tabBarItem.title
. With this, you can easily modify how the ViewController is displayed by simply changing the TabBarType
‘s value. It is a convenient approach as it also lets you assign/remove TabBarType
properties. Here’s a sample of what it would look like in implementation:
//somewhere in you codelet viewController = SearchViewController()viewController.tabBarType = .searchtabBarController.viewControllers.append(viewController)
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController.tabBarType == .search {
//do something with the search tab here?
}
return true
}
Not a fan of the solution above? Another way is going further and removing TabBarChild
‘s set
access, leaving the conforming object with an immutable TabBarType
property so their implementation with sort of look like this:
protocol TabBarChild {
var tabBarType: TabBarType { get }
}
extension AccountsViewController: TabBarChild {
var tabBarType: TabBarType {
return .accounts
}
}
This lets you assign TabBarType
properties to your child ViewControllers more securely. On your TabBar management layer, accessing them may look like this:
//sample UITabBarDelegate callsextension HomeFlowController: UITabBarControllerDelegate {
func tabBarController(\_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let tabBarChild = viewController as? TabBarChild,
tabBarChild.tabBarType == .accounts {
//do something with accounts tab here?
}
return true
}
}
Since it is agnostic about your child ViewController type, you can easily keep the changes in your ViewController implementation from directly affecting your Tab Management logic.
The solutions I presented here can definitely be further expanded so try and work around other possibilities. See which one may work best for you and your team. Would adding more properties/functions to TabBarChild
protocol help you better? Or would a more simplified version do the job just right?
And if you got your own solutions, please feel free to share them here and I’m very interested to hear about them!
I hope that you got what you’re looking for when you opened this article. If so, I would very much appreciate it if you would recommend this to your friends ^^
if you have any more questions/feedbacks, please feel free to drop a message!