Creating a customized tab bar in iOS with Swift

Boleigha Mark
Sprinthub Engineering
6 min readNov 17, 2019

So you have just received the mockups and design specs for your next iOS app and everything is looking pretty standard. But wait, something is off, the tab navigation menu is unlike the default iOS implementation.

Following Apple’s human interface design guidelines, the tab bar controller is not very customizable and recommends only 5 icons before having to present a “more options” icon. The default implementation is pretty straightforward and could look like any of these:

Tab navigation examples with default icons

So what then happens when you receive a mockup that looks like this:

Custom navigation bar with custom icons and no tint color

Notable differences in this mockup are the absence of a tint color on the selected item and the use of a custom selected tab indicator. While the tab bar may support various levels of customization, it leaves very little open for modification. This leaves us with one simple option, create our own tab menu. enough talk lets code.

First, create a project as you would normally do in Xcode. go to your Main.storyboard (or whatever you are calling the storyboard holding your views) and embed your starting/initial view controller in a tab bar controller.

Next, we need an enum to store our tab items so only one can be selected or returned at a time, create a file called TabItem.swift and paste the following code in it:

import UIKitenum TabItem: String, CaseIterable {
case calls = "calls"
case photos = "photos"
case contacts = "friends"
case messages = "messages"
var viewController: UIViewController {
switch self {
case .calls:
return CallsViewController()

case .contacts:
return ContactsViewController()
case .photos:
return PhotosViewController()
case .messages:
return InboxViewController()
}
}
// these can be your icons
var icon: UIImage {
switch self {
case .calls:
return UIImage(named: "ic_phone")!

case .photos:
return UIImage(named: "ic_camera")!
case .contacts:
return UIImage(named: "ic_contacts")!
case .messages:
return UIImage(named: "ic_message")!
}
}
var displayTitle: String {
return self.rawValue.capitalized(with: nil)
}
}

What the code above does is quite simple, it defines all of our menu items. Because it is an enum, it can only return one source of truth; in this case, a view controller corresponding to the view we want to show when a tab item is selected, its icon and its display title. Your code will have a lot of errors at this point because the view controllers being called are not yet created. quickly create them as “Cocoa touch classes” with the view controller names from “TabItem.swift” (i.e CallsViewController, ContactsViewController …) making sure not to select the “create xib file option” as we will not be using it in this tutorial.

Now create a new swift file called “NavigationBaseController.swift”. This will be our default navigation handler. Next, go to your “main.storyboard file, select the Tab bar controller and set its class to “NavigationBaseController”. this will allow us to fully control the tab bar and its behavior. paste the following code in NavigationBaseController.swift:

import UIKitclass NavigationMenuBaseController: UITabBarController {
var customTabBar: TabNavigationMenu!
var tabBarHeight: CGFloat = 67.0
override func viewDidLoad() {
super.viewDidLoad()
self.loadTabBar()
}
func loadTabBar() {
// We'll create and load our custom tab bar here
}
func setupCustomTabMenu(_ menuItems: [TabItem], completion: @escaping ([UIViewController]) -> Void) {// handle creation of the tab bar and attach touch event listeners
}
func changeTab(tab: Int) {
self.selectedIndex = tab
}
}

In this file, we are extending the default TabBarController class as our concern is only the tab bar. As you may have noticed, the customTabBar variable is of a type that does not exist. Create a new Swift file called “TabNavigationMenu.swift” and paste the following in it

import UIKitclass TabNavigationMenu: UIView {var itemTapped: ((_ tab: Int) -> Void)?
var activeItem: Int = 0

override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
convenience init(menuItems: [TabItem], frame: CGRect) {
self.init(frame: frame)
// ...
}
func createTabItem(item: TabItem) {
// ...
}
@objc func handleTap(_ sender: UIGestureRecognizer) {
// ...
}
func switchTab(from: Int, to: Int) {
// ...
}
func activateTab(tab: Int) {
// ...
}
func deactivateTab(tab: Int) {
// ...
}
}

There, we have the skeleton of our custom tab bar. Now let’s make it work like it is supposed to. We’ll begin with our navigation base controller. paste the following code in our “loadTabBar” function:

let tabItems: [TabItem] = [.messages, .calls, .contacts, .photos]self.setupCustomTabBar(tabItems) { (controllers) in
self
.viewControllers = controllers
}
self.selectedIndex = 0 // default our selected index to the first item

Next, paste the following into your setupCustomTabBar() method in your NavigationBaseController class

let frame = tabBar.frame
var controllers = [UIViewController]()
// hide the tab bar
tabBar.isHidden = true
self.customTabBar = TabNavigationMenu(menuItems: items, frame: frame)
self.customTabBar.translatesAutoresizingMaskIntoConstraints = false
self
.customTabBar.clipsToBounds = true
self
.customTabBar.itemTapped = self.changeTab
// Add it to the view
self.view.addSubview(customTabBar)
// Add positioning constraints to place the nav menu right where the tab bar should be
NSLayoutConstraint.activate([
self.customTabBar.leadingAnchor.constraint(equalTo: tabBar.leadingAnchor),
self.customTabBar.trailingAnchor.constraint(equalTo: tabBar.trailingAnchor),
self.customTabBar.widthAnchor.constraint(equalToConstant: tabBar.frame.width),
self.customTabBar.heightAnchor.constraint(equalToConstant: tabBarHeight), // Fixed height for nav menu
self.customTabBar.bottomAnchor.constraint(equalTo: tabBar.bottomAnchor)
])
for i in 0 ..< items.count {
controllers.append(items[i].viewController) // we fetch the matching view controller and append here
}
self.view.layoutIfNeeded() // important step
completion(controllers) // setup complete. handoff here

Now, go to TabNavigationMenu.swift, paste this in your convenience init() method:

// Convenience init bodyself.layer.backgroundColor = UIColor.white.cgColorfor i in 0 ..< menuItems.count {
let itemWidth = self.frame.width / CGFloat(menuItems.count)
let leadingAnchor = itemWidth * CGFloat(i)

let itemView = self.createTabItem(item: menuItems[i])
itemView.translatesAutoresizingMaskIntoConstraints = false
itemView.clipsToBounds = true
itemView.tag = i
self.addSubview(itemView)NSLayoutConstraint.activate([
itemView.heightAnchor.constraint(equalTo: self.heightAnchor),
itemView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: leadingAnchor),
itemView.topAnchor.constraint(equalTo: self.topAnchor),
])
}
self.setNeedsLayout()
self.layoutIfNeeded()
self.activateTab(tab: 0) // activate the first tab

Paste this in the createTabItem() method:

// Create a custom nav menu item    
func createTabItem(item: MenuItem) -> UIView {
let tabBarItem = UIView(frame: CGRect.zero)
let itemTitleLabel = UILabel(frame: CGRect.zero)
let itemIconView = UIImageView(frame: CGRect.zero)
itemTitleLabel.text = item.displayTitle
itemTitleLabel.textAlignment = .center
itemTitleLabel.translatesAutoresizingMaskIntoConstraints = false
itemTitleLabel.clipsToBounds = true

itemIconView.image = item.icon!.withRenderingMode(.automatic)
itemIconView.translatesAutoresizingMaskIntoConstraints = false
itemIconView.clipsToBounds = true
tabBarItem.layer.backgroundColor = UIColor.white.cgColor
tabBarItem.addSubview(itemIconView)
tabBarItem.addSubview(itemTitleLabel)
tabBarItem.translatesAutoresizingMaskIntoConstraints = false
tabBarItem.clipsToBounds = true
NSLayoutConstraint.activate([
itemIconView.heightAnchor.constraint(equalToConstant: 25), // Fixed height for our tab item(25pts)
itemIconView.widthAnchor.constraint(equalToConstant: 25), // Fixed width for our tab item icon
itemIconView.centerXAnchor.constraint(equalTo: tabBarItem.centerXAnchor),
itemIconView.topAnchor.constraint(equalTo: tabBarItem.topAnchor, constant: 8), // Position menu item icon 8pts from the top of it's parent view
itemIconView.leadingAnchor.constraint(equalTo: tabBarItem.leadingAnchor, constant: 35),
itemTitleLabel.heightAnchor.constraint(equalToConstant: 13), // Fixed height for title label
itemTitleLabel.widthAnchor.constraint(equalTo: tabBarItem.widthAnchor), // Position label full width across tab bar item
itemTitleLabel.topAnchor.constraint(equalTo: itemIconView.bottomAnchor, constant: 4), // Position title label 4pts below item icon
])
tabBarItem.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.handleTap))) // Each item should be able to trigger and action on tapreturn tabBarItem
}

replace handleTap(), deactivateTab(), switchTab() and activateTab() with the following:

@objc func handleTap(_ sender: UIGestureRecognizer) {
self.switchTab(from: self.activeItem, to: sender.view!.tag)
}
func switchTab(from: Int, to: Int) {
self.deactivateTab(tab: from)
self.activateTab(tab: to)
}
func activateTab(tab: Int) {
let tabToActivate = self.subviews[tab]
let borderWidth = tabToActivate.frame.size.width - 20
let borderLayer = CALayer()
borderLayer.backgroundColor = UIColor.green.cgColor
borderLayer.name = "active border"
borderLayer.frame = CGRect(x: 10, y: 0, width: borderWidth, height: 2)
DispatchQueue.main.async {
UIView.animate(withDuration: 0.8, delay: 0.0, options: [.curveEaseIn, .allowUserInteraction], animations: {
tabToActivate.layer.addSublayer(borderLayer)
tabToActivate.setNeedsLayout()
tabToActivate.layoutIfNeeded()
})
self.itemTapped?(tab)
}
self.activeItem = tab
}
func deactivateTab(tab: Int) {
let inactiveTab = self.subviews[tab]
let layersToRemove = inactiveTab.layer.sublayers!.filter({ $0.name == "active border" })
DispatchQueue.main.async {
UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseIn, .allowUserInteraction], animations: {
layersToRemove.forEach({ $0.removeFromSuperlayer() })
inactiveTab.setNeedsLayout()
inactiveTab.layoutIfNeeded()
})
}
}

finally, paste this into all of your view controllers onViewDidLoad() methods swap [CONTROLLER_NAME] with the name of your controller

self.view.backgroundColor = UIColor.whitelet label = UILabel(frame: CGRect.zero)
label.text = "[CONTROLLER_NAME] View Controller"
label.font = UIFont.systemFont(ofSize: 16)
label.translatesAutoresizingMaskIntoConstraints = false
label.clipsToBounds = true
label.sizeToFit()
self.view.addSubview(label)NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])

Voila. now run your app. the final result should look like this

Cool huh? this is just a small demo, there is no limit to what you can do with this as it is a UIView. Take it further, add some animations, circles and whatever comes to mind but do not stray far from Apple’s Human Interface Guidelines as they will make your app more accessible and keep the user experience as fluid as possible.

The code is on Github. feel free to improve it and send a PR.

--

--