UIKit12 min readJun 30, 2026

Mastering UITabBarController: The Heart of iOS App Navigation

The UITabBarController is a fundamental component for iOS app navigation, offering a clear and intuitive way to switch between distinct functional areas of your application. This guide will walk you through setting up, customizing, and mastering its capabilities to build robust and user-friendly interfaces.

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.

swift
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.

swift
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.

swift
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.

swift
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:

  1. 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.
  2. Consistency: Ensure each tab represents a distinct, primary function of your app, not just a filter or sub-feature.
  3. 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.
  4. Meaningful Icons and Titles: Use clear, concise titles and easily recognizable icons (preferably SF Symbols) for each UITabBarItem.
  5. Accessibility: UITabBarController is inherently accessible. Ensure your custom icons and titles are also designed with accessibility in mind.
  6. 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.
  7. 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.

Relying on old UITabBar customization

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: Relying on old UITabBar customization

Developers often use deprecated `UITabBar` properties like `barTintColor` and `tintColor` from older iOS versions, leading to inconsistent UI or issues on newer platforms, especially with scroll behavior.

swift
tabBar.barTintColor = .black
tabBar.tintColor = .white

TASK HIERARCHY: UITabBarController Layout

The UITabBarController manages its child view controllers and adjusts their frames to accommodate the tab bar at the bottom. It handles transitions betweeen tabs and manages the 'More' tab automatically.

UIWindow
MainTabBarController (UITabBarController)
└─ (Selected View Controller's View)
└─ UITabBar
1

1. Root view controllers setup

You provide an array of `UIViewController` instances.

2

2. Tab Bar Item Creation

Each `UIViewController`'s `tabBarItem` property defines its tab's appearance.

3

3. View Hiearchy Integration

The `UITabBarController` adds the selected child's view while keeping the tab bar visible.

4

4. Appearance Management

Handles global and per-tab customization through `UITabBarAppearance`.

Visualized execution hierarchy.

Powerful Guarantees

Automatic Layout

Correctly sizes and positions child views, reserving space for the tab bar.

Persistent State

Child view controller states are preserved when switching tabs.

Accessibility Support

Standard accessibility for tab switching and item labels.

REAL PRODUCTION EXAMPLE: Inconsistent Tab Bar Appearance

A financial app had its tab bar disappear or show a default white background when scrolling content on iOS 15 devices, despite `barTintColor` being set to black. This was due to `scrollEdgeAppearance` not being configured.

Impact / Results
Jumping tab bar appearance
Visually jarring transitions
Inconsistent brand experience
THE FIX or SOLUTION
swift
if #available(iOS 15.0, *) {
    let appearance = UITabBarAppearance()
    appearance.configureWithOpaqueBackground()
    appearance.backgroundColor = .black // Set desired background color
    
    // Customize item colors and text attributes here
    // ...
    
    self.tabBar.standardAppearance = appearance
    self.tabBar.scrollEdgeAppearance = appearance // Crucial for scroll views
} else {
    self.tabBar.barTintColor = .black
    self.tabBar.tintColor = .white
    self.tabBar.unselectedItemTintColor = .systemGray
}

INTERVIEW PERSPECTIVE

Common Question

Explain how you would customize a UITabBarController's appearance, specifically addressing iOS 15+ compatibility.

Strong Answer

A strong answer demonstrates understanding of both legacy and modern appearance APIs. It highlights the use of `UITabBarAppearance` (`standardAppearance`, `scrollEdgeAppearance`) for iOS 15+ to configure background, item colors, and text attributes. It would also mention the fallback to `barTintColor`, `tintColor`, and `unselectedItemTintColor` for older iOS versions via `#available` checks, ensuring a consistent user experience across supported OS versions.

Interviewers Expect you to understand:
  • `standardAppearance` and `scrollEdgeAppearance`
  • `UITabBarAppearance` properties (backgroundColor, itemAppearance)
  • Handling iOS 15+ vs. older versions
  • Code example or clear explanation of the API used
KEY TAKEAWAY

Always use `UITabBarAppearance` for robust and consistent `UITabBarController` customization on iOS 15+, and remember to set both `standardAppearance` and `scrollEdgeAppearance` to avoid visual glitches, especially with scrolling content.

Frequently Asked Questions

Can I hide the tab bar temporarily?
Yes, you can hide the tab bar for a specific view controller within one of the tabs by setting `hidesBottomBarWhenPushed = true` on that view controller before pushing it onto its navigation stack. For example, `detailVC.hidesBottomBarWhenPushed = true; navigationController?.pushViewController(detailVC, animated: true)`.
How do I programmatically switch between tabs?
You can programmatically select a tab by setting the `selectedIndex` property of the `UITabBarController`. For example, `myTabBarController.selectedIndex = 1` will select the second tab. Alternatively, you can set `selectedViewController` property to the `UIViewController` you want to display.
How do I customize the appearance of the 'More' tab?
When you have more than 5 view controllers, the `UITabBarController` automatically creates a 'More' tab that presents a `UINavigationController` containing a list of the additional view controllers. You can customize the appearance of the `More` tab's navigation controller using `UINavigationBarAppearance` or by accessing the `moreNavigationController` property of the `UITabBarController`.
What is the difference between `tintColor` and `barTintColor` on `UITabBar`?
`barTintColor` (pre-iOS 15) affects the background color of the tab bar itself. `tintColor` affects the color of the selected tab bar item (its image and text). For iOS 15 and later, these properties are largely superseded by using `UITabBarAppearance` and configuring its `backgroundColor`, `stackedLayoutAppearance.selected.iconColor`, and `stackedLayoutAppearance.selected.titleTextAttributes`.
Can I use a custom `UITabBar` class?
While you can technically subclass `UITabBarController` and override some methods, directly replacing or heavily customizing the `UITabBar` subview within the `UITabBarController` is generally discouraged by Apple. If you need a completely custom tab-like interface that deviates significantly from the standard `UITabBarController` behavior, it's often better to implement a custom container view controller from scratch or use a library, rather than fighting the `UITabBarController`'s internal structure.
#UIKit#UITabBarController#iOS Development#Navigation#Swift#User Interface