Tab Bar Controller Guide
Overview
Tab bar controllers are implemented by the
UITabBarController class. They allow a user to
switch between multiple arbitrary view controllers by maintaining an
array of UIViewControllers.
They can be used to allow the user to navigate between entirely
different parts of your application, or they can be used to display two
different views of the same backing data. Tab bar controllers also have
the built-in ability to display a “More…” interface when more than 5
tabs are added. They can also let the user customize which tabs are
shown on the main tab bar when there are more than 5 tabs. An
example of this behavior is found in the standard iOS “Music” app.
This guide covers common use cases for tab bar controllers. A more in
depth guide by Apple can be found here
In order to demonstrate basic functionality of the UITabBarController
we will build a very basic clone of the standard iOS “Clock” app. Our
app will have only two tabs one to display the current time, and the
other will implement the stopwatch functionality.
Using tab bar controllers in storyboards
We create a new project from the iOS App template (in Xcode 11 and
earlier this template was called Single View Application / Single
View App; Apple consolidated several iOS app templates into one App
template in Xcode 12) and add two subclasses of
UIViewController via File -> New -> iOS -> Source -> Cocoa Touch Class. We create a ClockViewController and a
StopwatchViewController.
We open up our Main.storyboard and go ahead and delete the
pregenerated view controller. Dragging a “Tab Bar Controller” from the
Object Library into the storyboard automatically creates two view
controllers that are already in the tab bar controller’s array of view
controllers. We also set the tab bar controller to be our
application’s root view controller selecting it and by ticking the Is Initial View Controller checkbox.
Adding tabs to the tab bar controller
We can add more tabs by dragging a new view controller from
the Object Library onto the storyboard and then control-dragging from
the tab bar controller to the new view controller and then selecting
Relationship Segues -> view controllers. This will add the new view
controller to the tab bar controller’s array of view controllers. It
also automatically adds a tab bar item to our new view controller which
will allow us to configure the appearance of the button that represents
this view controller in the tab bar.
In our case we only needed two view controllers total, but we removed
and readded one above to illustrate the technique.
We set the Custom Class property of one of the view controllers in our
tab bar to ClockViewController and the other to
StopwatchViewController. Now we can design our view controllers and
connect @IBOutlets as we would any other view controller.
We add a single label to the ClockViewController. It will display the
current time. We add a label to the StopwatchViewController to
display the elapsed time and add two buttons for “start” and
“stop/reset”. We add @IBOutlets for our two labels and add
@IBActions to respond to each button being tapped.
Finally we can add the code to make our clock tick
class ClockViewController: UIViewController {
@IBOutlet weak var timeLabel: UILabel!
var timer: Timer?
let dateFormatter = DateFormatter()
func updateTime() {
timeLabel.text = dateFormatter.string(from: Date())
}
override func viewDidLoad() {
super.viewDidLoad()
dateFormatter.dateFormat = "hh:mm:ss"
updateTime()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateTime()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
}
}
// ClockViewController.m
#import "ClockViewController.h"
@interface ClockViewController ()
@property (weak, nonatomic) IBOutlet UILabel *timeLabel;
@end
@implementation ClockViewController
NSTimer *clockTimer;
NSDateFormatter *clockDateFormatter;
- (void)updateTime{
self.timeLabel.text = [clockDateFormatter stringFromDate:[NSDate date]];
}
- (void)viewDidLoad {
[super viewDidLoad];
clockDateFormatter = [[NSDateFormatter alloc] init];
clockDateFormatter.dateFormat = @"hh:mm:ss";
[self updateTime];
}
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
clockTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTime) userInfo:nil repeats:true];
}
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[clockTimer invalidate];
}
@end
We also need to add a stop watch controller:
import UIKit
class StopwatchViewController: UIViewController {
@IBOutlet weak var elapsedTimeLabel: UILabel!
let dateFormatter = DateFormatter()
var elapsedTimeAtStop: TimeInterval = 0
var dateAtStart: Date?
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
dateFormatter.dateFormat = "mm:ss.S"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
updateElapsedTime()
}
func updateElapsedTime() {
elapsedTimeLabel.text = dateFormatter.string(from: dateForFormatter())
}
private func dateForFormatter() -> Date {
if let startDate = self.dateAtStart {
let intervalSinceStart = Date().timeIntervalSince(startDate)
let totalElapsedTime = elapsedTimeAtStop + intervalSinceStart
return Date(timeIntervalSince1970: totalElapsedTime)
}
return Date(timeIntervalSince1970: elapsedTimeAtStop)
}
@IBAction func startButtonTapped(_ sender: Any) {
if dateAtStart == nil {
dateAtStart = Date()
timer = Timer.scheduledTimer(withTimeInterval: 1.0/10.0, repeats: true) { [weak self] _ in
self?.updateElapsedTime()
}
}
}
@IBAction func stopButtonTapped(_ sender: Any) {
if let startDate = dateAtStart { // stop
elapsedTimeAtStop += Date().timeIntervalSince(startDate)
} else { // reset
elapsedTimeAtStop = 0
}
timer?.invalidate()
timer = nil
dateAtStart = nil
updateElapsedTime()
}
}
// StopwatchViewController.m
#import "StopwatchViewController.h"
@interface StopwatchViewController ()
@property (weak, nonatomic) IBOutlet UILabel *elapsedTimeLabel;
- (IBAction)startButtonTapped:(UIButton *)sender;
- (IBAction)stopButtonTapped:(UIButton *)sender;
@end
@implementation StopwatchViewController
NSDateFormatter *dateFormatter;
NSTimeInterval elapsedTimeAtStop = 0;
NSDate *dateAtStart = NULL;
NSTimer *timer;
- (void)viewDidLoad {
[super viewDidLoad];
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"mm:ss.S";
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
[self updateElapsedTime];
}
-(void)updateElapsedTime{
self.elapsedTimeLabel.text = [dateFormatter stringFromDate: [self dateForFormatter]];
}
-(NSDate*)dateForFormatter{
if(dateAtStart != NULL){
NSTimeInterval intervalSinceStart = [[NSDate date] timeIntervalSinceDate:dateAtStart];
NSTimeInterval totalElapsedTime = elapsedTimeAtStop + intervalSinceStart;
return [NSDate dateWithTimeIntervalSince1970:totalElapsedTime];
}
return [NSDate dateWithTimeIntervalSince1970:elapsedTimeAtStop];
}
- (IBAction)startButtonTapped:(UIButton *)sender {
if(dateAtStart == NULL){
dateAtStart = [NSDate date];
timer = [NSTimer scheduledTimerWithTimeInterval:1.0/10.0 target:self selector:@selector(updateElapsedTime) userInfo:nil repeats:true];
}
}
- (IBAction)stopButtonTapped:(UIButton *)sender {
if(dateAtStart != NULL){ //stop
elapsedTimeAtStop += [[NSDate date] timeIntervalSinceDate:dateAtStart];
}
else{ //reset
elapsedTimeAtStop = 0;
}
[timer invalidate];
timer = NULL;
dateAtStart = NULL;
[self updateElapsedTime];
}
@end
Programmatically setting up a tab bar controller
If you have 2 view controllers vc1 and vc2 created programmatically, then you can instantiate a UITabBarController and initialize it like this:
let tabBarController = UITabBarController()
tabBarController.viewControllers = [vc1, vc2]
UITabBarController *tabBarController = [[UITabBarController alloc]init];
tabBarController.viewControllers = @[vc1, vc2];
Before we set up tab view controller programmatically, we need to remove the tab view
controller that we added in the Interface Builder (IB).
In IB, remove the segues you have from tab bar view controller to both clock view controller
and stop watch view controller. After that delete the tab view controller
altogether from IB.
Your story board should look something like this now:
When the app starts up, the system connects a scene to the app and calls our
scene delegate’s scene(_:willConnectTo:options:)
method (Objective-C: scene:willConnectToSession:options:). This is where we
can do any custom view controller set up: we create a window attached to the
scene with UIWindow(windowScene:)
and set its root view controller. Since we now create the window ourselves,
also delete the Storyboard Name (UISceneStoryboardFile) entry under
Application Scene Manifest in the Info.plist so the system doesn’t try to
load the storyboard’s initial view controller — we still instantiate our view
controllers from the storyboard manually.
This is sample code from SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// confirm the scene is a window scene and create a UIWindow attached to it
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
// create story board. Default story board will be named as Main.storyboard in your project.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
// create view controllers from storyboard
// Make sure you set Storyboard ID for both the viewcontrollers in
// Interface Builder -> Identity Inspector -> Storyboard ID
let clockViewController = storyboard.instantiateViewController(withIdentifier: "ClockViewController")
let stopWatchViewController = storyboard.instantiateViewController(withIdentifier: "StopWatchViewController")
// Set up the Tab Bar Controller to have two tabs
let tabBarController = UITabBarController()
tabBarController.viewControllers = [clockViewController, stopWatchViewController]
// Make the Tab Bar Controller the root view controller
window?.rootViewController = tabBarController
window?.makeKeyAndVisible()
}
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
// confirm the scene is a window scene and create a UIWindow attached to it
if (![scene isKindOfClass:[UIWindowScene class]]) { return; }
self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];
// create story board. Default story board will be named as Main.storyboard in your project.
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
// create view controllers from storyboard
// Make sure you set Storyboard ID for both the viewcontrollers in
// Interface Builder -> Identity Inspector -> Storyboard ID
ClockViewController *clockViewController = [storyboard instantiateViewControllerWithIdentifier:@"ClockViewController"];
StopWatchViewController *stopWatchViewController = [storyboard instantiateViewControllerWithIdentifier:@"StopWatchViewController"];
// Set up the Tab Bar Controller to have two tabs
UITabBarController *tabBarController = [[UITabBarController alloc]init];
tabBarController.viewControllers = @[clockViewController, stopWatchViewController];
// Make the Tab Bar Controller the root view controller
self.window.rootViewController = tabBarController;
[self.window makeKeyAndVisible];
}
Note: Before the scene-based life cycle, apps created the window in the app delegate’s
didFinishLaunchingWithOptionsmethod withUIWindow(frame: UIScreen.main.bounds)(Objective-C:[[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]), and you may still see that pattern in older tutorials. It no longer works: as of the iOS 27 SDK the scene-based life cycle is required — per the iOS & iPadOS 27 release notes, apps built with the latest SDK must adopt it or they fail to launch — andUIScreen.main(Objective-C:UIScreen.mainScreen) is itself deprecated as of iOS 26. Apple’s TN3187: Migrating to the UIKit scene-based life cycle covers migrating code like this.
Make sure you provide story board id for both clock and stop watch view controllers
in IB so that they can be initialized in code:
It is very common for a tab bar controller to have navigation controllers as root
view controllers. If this is the case, you will want to assign the storyboard ID
to the navigation controller instead of the view controller directly. You can access
the top view controller in the navigation stack to do any additional configuration.
let nav = storyboard.instantiateViewController(withIdentifier: "ExampleNavigationController") as! UINavigationController
let vc = nav.topViewController as! ClockViewController
// perform any initial configuration of vc here...
UINavigationController *nav = [storyboard instantiateViewControllerWithIdentifier:@"ExampleNavigationController"];
ClockViewController *vc = (ClockViewController *)nav.topViewController;
// perform any initial configuration of vc here...
Responding to a tab being selected
to be completed
The “More..” button
The tab bar has limited space for displaying your custom items. If you add six or more
custom view controllers to a tab bar controller, the tab bar controller displays only
the first four items plus the standard More item on the tab bar.
Tapping the More item brings up a standard interface for selecting the remaining items.
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
let vc1 = setUpViewController(title: "First", backgroundColor: UIColor.orange)
let vc2 = setUpViewController(title: "Second", backgroundColor: UIColor.purple)
let vc3 = setUpViewController(title: "Third", backgroundColor: UIColor.red)
let vc4 = setUpViewController(title: "Fourth", backgroundColor: UIColor.green)
let vc5 = setUpViewController(title: "Fifth", backgroundColor: UIColor.blue)
let vc6 = setUpViewController(title: "Sixth", backgroundColor: UIColor.yellow)
// Set up the Tab Bar Controller
let tabBarController = UITabBarController()
tabBarController.viewControllers = [vc1, vc2, vc3, vc4, vc5, vc6]
// Make the Tab Bar Controller the root view controller
window?.rootViewController = tabBarController
window?.makeKeyAndVisible()
}
func setUpViewController(title: String, backgroundColor: UIColor) -> UIViewController {
let vc = UIViewController()
vc.view.backgroundColor = backgroundColor
vc.tabBarItem.title = title
vc.tabBarItem.image = UIImage(named: "star")
return vc
}
}
// SceneDelegate.h
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
// SceneDelegate.m
#import "SceneDelegate.h"
@implementation SceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
if (![scene isKindOfClass:[UIWindowScene class]]) { return; }
self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];
UIViewController *vc1 = [self setUpViewController:@"First" withBackgroundColor:UIColor.orangeColor];
UIViewController *vc2 = [self setUpViewController:@"Second" withBackgroundColor:UIColor.purpleColor];
UIViewController *vc3 = [self setUpViewController:@"Third" withBackgroundColor:UIColor.redColor];
UIViewController *vc4 = [self setUpViewController:@"Fourth" withBackgroundColor:UIColor.greenColor];
UIViewController *vc5 = [self setUpViewController:@"Fifth" withBackgroundColor:UIColor.blueColor];
UIViewController *vc6 = [self setUpViewController:@"Sixth" withBackgroundColor:UIColor.yellowColor];
// Set up the Tab Bar Controller
UITabBarController *tabBarController = [[UITabBarController alloc]init];
tabBarController.viewControllers = @[vc1, vc2, vc3, vc4, vc5, vc6];
// Make the Tab Bar Controller the root view controller
self.window.rootViewController = tabBarController;
[self.window makeKeyAndVisible];
}
- (UIViewController*) setUpViewController:(NSString*)title withBackgroundColor:(UIColor*)backgroundColor{
UIViewController *vc = [[UIViewController alloc]init];
vc.view.backgroundColor = backgroundColor;
vc.tabBarItem.title = title;
vc.tabBarItem.image = [UIImage imageNamed:@"star"];
return vc;
}
@end
In More tab, user can tap on Edit button. That will allow user to reorder
tabs so that they can pick which 4 view controllers will appear on the tab view
and which view controllers will be in More tab.
Configuring what is allowed to be reordered
By default, the user is allowed to rearrange all items on the tab bar.
You can specify what view controllers user can rearrange by setting
the customizableViewControllers property.
By default, all the items added to tab bar are also added to customizableViewControllers.
If we don’t want to allow user to change the view controllers for the
first 2 tabs, then we can set customizableViewControllers to the other
4 view controllers:
let tabBarController = UITabBarController()
tabBarController.viewControllers = [vc1, vc2, vc3, vc4, vc5, vc6]
tabBarController.customizableViewControllers = [vc3, vc4, vc5, vc6]
UITabBarController *tabBarController = [[UITabBarController alloc]init];
tabBarController.viewControllers = @[vc1, vc2, vc3, vc4, vc5, vc6];
tabBarController.customizableViewControllers = @[vc3, vc4, vc5, vc6];
In More tab -> Edit, user can now only see and reorder 3rd, 4th, 5th, 6th tabs.
- add screenshot -
Customizing the appearance of the tab bar
to be completed







