What is UITabBarController?
The UITabBarController is a container view controller that manages multiple view controllers. It presents their views in a tab bar interface, allowing users to select a specific view controller to display. Each tab corresponds to a single root view controller that resides within the tab bar controller's view hierarchy. This pattern is ideal for applications with distinct, parallel content areas that users frequently switch between.
Think of the Photos app or the App Store app; they both use UITabBarController to navigate between main sections like 'Library', 'For You', 'Albums', 'Search' (Photos) or 'Today', 'Games', 'Apps' (App Store). It provides a persistent navigation mechanism at the bottom of the screen, ensuring that users always know where they are and how to navigate to other primary sections.
Setting Up UITabBarController Programmatically
Setting up a UITabBarController programmatically involves creating an instance of UITabBarController, instantiating the view controllers you want to manage, and then assigning them to the tab bar controller's viewControllers property. Each view controller will appear as a tab item in the tab bar. It's common practice to embed these root view controllers within UINavigationController instances to provide proper navigation stacks within each tab.
Let's walk through a basic example. We'll create three simple view controllers, embed two of them in navigation controllers, and then assign them to a UITabBarController.
import UIKit
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemRed
title = "First Tab"
let label = UILabel()
label.text = "Red View"
label.textColor = .white
label.font = .preferredFont(forTextStyle: .largeTitle)
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
tabBarItem = UITabBarItem(title: "First", image: UIImage(systemName: "1.circle"), selectedImage: UIImage(systemName: "1.circle.fill"))
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGreen
title = "Second Tab"
let label = UILabel()
label.text = "Green View"
label.textColor = .white
label.font = .preferredFont(forTextStyle: .largeTitle)
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
tabBarItem = UITabBarItem(title: "Second", image: UIImage(systemName: "2.circle"), selectedImage: UIImage(systemName: "2.circle.fill"))
}
}
class ThirdViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
title = "Third Tab"
let label = UILabel()
label.text = "Blue View"
label.textColor = .white
label.font = .preferredFont(forTextStyle: .largeTitle)
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
tabBarItem = UITabBarItem(title: "Third", image: UIImage(systemName: "3.circle"), selectedImage: UIImage(systemName: "3.circle.fill"))
}
}
class MainTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Create instances of your view controllers
let vc1 = FirstViewController()
let vc2 = SecondViewController()
let vc3 = ThirdViewController()
// Embed them in navigation controllers if needed
let nav1 = UINavigationController(rootViewController: vc1)
let nav2 = UINavigationController(rootViewController: vc2)
// For the third one, let's keep it without a NavigationController for variety
// Assign the view controllers to the tab bar controller
// The order in the array determines the order of the tabs
self.viewControllers = [nav1, nav2, vc3]
// Configure the tab bar appearance (optional)
self.tabBar.tintColor = .systemYellow
self.tabBar.unselectedItemTintColor = .systemGray
// iOS 15+ appearance customization
if #available(iOS 15.0, *) {
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .secondarySystemBackground
// Set colors for selected and unselected item states
appearance.stackedLayoutAppearance.selected.iconColor = .systemYellow
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.systemYellow]
appearance.stackedLayoutAppearance.normal.iconColor = .systemGray
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.systemGray]
self.tabBar.standardAppearance = appearance
self.tabBar.scrollEdgeAppearance = appearance
}
}
}
// To make this the root view controller in your SceneDelegate or AppDelegate:
//
// func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// guard let windowScene = (scene as? UIWindowScene) else { return }
// window = UIWindow(windowScene: windowScene)
// window?.rootViewController = MainTabBarController()
// window?.makeKeyAndVisible()
// }
Customizing Tab Bar Items
Each tab in the UITabBarController is represented by a UITabBarItem. You can customize the UITabBarItem properties, such as the title, image, and selected image, to provide a clear indication of the content for each tab. It's crucial to set appropriate tabBarItem properties for each root view controller you manage within the UITabBarController.
Starting from iOS 13, you can use SF Symbols for a more modern and consistent look. For selectedImage, if you don't explicitly set it, the system will render the image using the tab bar's tint color when selected. However, providing a selectedImage allows for distinct visuals, e.g., a filled vs. outline icon.
import UIKit
class CustomTabViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPurple
title = "Settings"
// Initialize UITabBarItem with SF Symbols (iOS 13+)
tabBarItem = UITabBarItem(
title: "Settings",
image: UIImage(systemName: "gearshape"),
selectedImage: UIImage(systemName: "gearshape.fill")
)
// You can also set a badge value for notifications or new content
tabBarItem.badgeValue = "3"
tabBarItem.badgeColor = .systemRed // Customize badge background color
}
}
// To integrate this with the previous MainTabBarController:
// let vc4 = CustomTabViewController()
// self.viewControllers = [nav1, nav2, vc3, vc4]
// To remove a badge:
// myViewController.tabBarItem.badgeValue = nil
Handling Tab Selection and Delegation
The UITabBarControllerDelegate protocol allows you to respond to tab selection events and control whether a tab can be selected. This is useful for implementing custom transitions, performing actions before switching tabs, or preventing selection under certain conditions (e.g., if a user isn't logged in).
The delegate methods provide opportunities to intervene in the tab selection process. For instance, tabBarController(_:shouldSelect:) allows you to ask for confirmation before switching tabs, or tabBarController(_:didSelect:) informs you after a tab has been successfully selected. Setting the delegate is straightforward.
import UIKit
class MyCustomTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Set your view controllers here, similar to the MainTabBarController example
let vc1 = FirstViewController()
let vc2 = SecondViewController()
let nav1 = UINavigationController(rootViewController: vc1)
let nav2 = UINavigationController(rootViewController: vc2)
self.viewControllers = [nav1, nav2]
// Assign the delegate
self.delegate = self
// Optional: Programmatically select a tab
self.selectedIndex = 0 // Selects the first tab (index 0) on load
}
// MARK: - UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// Example: Prevent selecting the second tab without a specific condition
if viewController === tabBarController.viewControllers?[1] {
print("Attempting to select the second tab. Checking conditions...")
// You could introduce logic here, e.g., check for user authentication
// return false // uncomment to prevent selection
return true // Allow selection for now
}
print("Selected tab: \(viewController.title ?? "Unknown")")
return true // Allow selection for all other tabs
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
// This method is called after a tab has been selected
print("Successfully selected tab: \(viewController.title ?? "Unknown")")
// You could perform logging, analytics, or UI updates here
}
}
More Tab Bar Customization (iOS 13+ and iOS 15+)
Modern iOS versions provide robust customization options for UITabBar. For iOS 15 and later, standardAppearance and scrollEdgeAppearance are essential for controlling how the tab bar looks in different states (e.g., when content scrolls under it). Prior to iOS 15, properties like barTintColor, tintColor, and unselectedItemTintColor were used.
To ensure your app's tab bar looks consistent and matches your brand, you should leverage these appearance APIs. The UITabBarAppearance class offers granular control over background, item colors, and item typography.
import UIKit
class AdvancedTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// (Assume view controllers are set up)
let vc1 = FirstViewController()
let vc2 = SecondViewController()
self.viewControllers = [vc1, vc2]
// iOS 15+ specific customization for tab bar appearance
if #available(iOS 15.0, *) {
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground() // Use transparent or opaque
appearance.backgroundColor = .systemMint // Background color for the tab bar
// Tab Bar Item Appearance (for normal state)
let normalAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.systemGray,
.font: UIFont.systemFont(ofSize: 10, weight: .regular)
]
appearance.stackedLayoutAppearance.normal.titleTextAttributes = normalAttributes
appearance.stackedLayoutAppearance.normal.iconColor = .systemGray
// Tab Bar Item Appearance (for selected state)
let selectedAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 10, weight: .semibold)
]
appearance.stackedLayoutAppearance.selected.titleTextAttributes = selectedAttributes
appearance.stackedLayoutAppearance.selected.iconColor = .white
// Apply the appearance
self.tabBar.standardAppearance = appearance
self.tabBar.scrollEdgeAppearance = appearance // Important for scroll views
} else {
// Fallback for iOS versions prior to 15
self.tabBar.barTintColor = .darkGray // Background color
self.tabBar.tintColor = .white // Selected item color
self.tabBar.unselectedItemTintColor = .lightGray // Unselected item color
}
}
}
Best Practices for UITabBarController
When working with UITabBarController, adhering to best practices ensures a good user experience and maintainable code:
- Keep it Simple: Limit the number of tabs to 5 or fewer. If you have more, the 'More' tab will automatically appear, which can complicate navigation.
- Consistency: Ensure each tab represents a distinct, primary function of your app, not just a filter or sub-feature.
- Embed in Navigation Controllers: For most tabs, it's best to embed the root view controller of each tab within its own
UINavigationController. This allows for hierarchical navigation within that tab without affecting other tabs.
- Meaningful Icons and Titles: Use clear, concise titles and easily recognizable icons (preferably SF Symbols) for each
UITabBarItem.
- Accessibility:
UITabBarController is inherently accessible. Ensure your custom icons and titles are also designed with accessibility in mind.
- Avoid Deep Nesting: While you can present modal view controllers or push new view controllers within a navigation stack on a tab, avoid excessive nesting that can disorient users.
- Dynamic Tab Bar Items: If you need to dynamically show/hide tabs based on user roles or app state, remember to update the
viewControllers array and handle any related state changes carefully. However, frequent changes can be jarring; consider if a UINavigationController or UISplitViewController might be more appropriate for highly dynamic content.