

For Swift developers looking to deepen their iOS expertise, mastering Apple’s core UI navigation components is essential. One of the most widely used—and instantly recognizable—navigation patterns in iOS apps is the UITabBarController. From productivity tools to social and finance apps, tab-based navigation provides a clean, intuitive way to organize multiple views within a single application.
In this article, you’ll learn how to implement a basic UITabBarController in Swift by building a simple app that displays the current time, local weather information, and your location on a map. This walkthrough is designed for Swift developers who want hands-on experience with UIKit navigation patterns.
As apps grow in complexity, ensuring that UI navigation and interactions work reliably across devices becomes increasingly important. This is where strong testing practices—and platforms like Tricentis Testim Mobile—play a critical role, helping teams validate UI behavior and user flows at scale.
UITabBarController basics
A UITabBarController, like all other UIController-based UI elements in an iOS developer’s toolset, is known as a navigation interface, or as Apple puts it: “A container view controller that manages a multiselection interface, where the selections determine which child view controller to display.”
A UITabBarController, like all other UIController-based UI elements in an iOS developer’s toolset, is known as a navigation interface, or as Apple puts it: “A container view controller that manages a multiselection interface, where the selections determine which child view controller to display.”
What is a TabController?
What this means is that, just like a hierarchy, the UITabBarController functions as a parent container handling the complexities of navigation for you while displaying only the child view the user is presently interacting with, offering a straightforward interface.
One screen is visible at a time, keeping the UI simple and more approachable, while the rest of the views are accessible at the touch of the tab buttons on the bottom of the screen.
Now, unless you’ve been living under a rock, you’ve seen these before. Here’s an example to ensure we’re all on the same page.

Simple, elegant, great.
Let’s build it.
Creating a UITabBarController
Open XCode and create a new iOS app. Select Storyboard as your design framework of choice, and move into the Main storyboard.

Note: If you want to know how to create great apps using SwiftUI, you should check out my other articles on the topic here.
Great! Now, you need to add the UITabBarController as the default controller, since the one that XCode starts you with is just a standard UIViewController.
Delete the original view controller and click on the plus (+) button on the top right of your developer tool. Then, search for the tab bar controller and simply drag it into the storyboard.

And there you go. You have everything you need for your new and fancy tab bar app. Well, mostly…
If you want to be adventurous and run your app, you’ll see the following:

Now, what’s going on here? First, let’s check the console for some clues.
![]()
All right, that’s helpful. This basically means that the app doesn’t have a view controller set as the starting point—a simple fix.
Click on the parent controller, and then, in the settings tab on the right side corner, tick the Is Initial View Controller box.

Now, rerun the code and…

All right. Now we’re talking!
Working on your UITabBarController
So far, you’ve mostly been playing around with the default features and setup XCode offers. No code has been written. But don’t worry, to actually use the power of the tab bar controller, you’ll be programming some views in it that are actually more useful than a blank screen.
Adding Some Views
The first view you’ll add to your tab bar controller app is a simple clock.
To simulate a clock, all you need is a single label that’s constantly updated with the device’s time every second. So, start by dropping a label and setting the view constraints and connections.

Now, create some Swift classes with their class definitions. You can call them item1.swift, item2.swift, and item3.swift on your project. Following that, go to the respective view on the storyboard and set the class of the view controller to be the class of item1.
Once you’ve done that, go to the class and add the following code:
//
// ViewController.swift
// TabBarControllerSample
//
// Created by Juan Mueller on 11/19/22.
// For more, visit www.ajourneyforwisdom.com
import UIKit
class ViewController: UIViewController {
// Outlet for label
@IBOutlet weak var clockLabel: UILabel!
// Date formatter
let dateFormatter = DateFormatter()
override func viewDidLoad() {
// Call the viewdidload super
super.viewDidLoad()
// Configure date formatter
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .medium
// Schedule a timer to trigger every second to invoke method updateLabel
Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(updateLabel),
userInfo: nil,
repeats:true);
}
// Method that updates the label text witht he current date time
@objc func updateLabel() -> Void {
// Update the text of the clock label with the time of the current date
clockLabel.text = dateFormatter.string(from: Date());
}
}
Finally, set the label referencing outlet to the label defined in the Swift class you created. Now, run your code, and you should see a clock running on the item1 tab.

Adding more views
For the second tab, let’s add a new controller: a table view controller. This controller will display some weather information on a table structure. Nothing complicated. Start by dropping a TableViewController from the items, just as you did with the TabBarController. Then, with the control button pressed, drag a connection between the TabBarController and the TableViewController, and set the relationship as a child view reference.
That’s how you add more views to your tab bar.
Following that, modify the code on your item2.swift class to the following:
//
// ViewController2.swift
// TabBarControllerSample
//
// Created by Juan Mueller on 11/19/22.
// For more, visit www.ajourneyforwisdom.com
import Foundation
import UIKit
class ViewController2: UIViewController, UITableViewDataSource, UITableViewDelegate {
// Outlet for tableView
@IBOutlet weak var tableView: UITableView!
// Weather data JSON property
var wdata: [String: Any]?
override func viewDidLoad() {
// Call the viewdidload super
super.viewDidLoad()
// Always register the tableview cell with the corresponding identifier in the storyboard
// so it can be reused
tableView?.register(UITableViewCell.self, forCellReuseIdentifier: "dataCell")
// Set the tableview datasource to self
tableView?.dataSource = self
// invoke the requestWeatherData method and handle its completion
requestWeatherData {
// Code inside this block will be executed in the main thread
DispatchQueue.main.async { [self] in
// Reload the tableview
tableView?.reloadData()
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Retrieve the registered reusable cell from the tableview
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "dataCell",
for: indexPath)
// Switch between the 4 possible rows to display
switch indexPath.item {
case 0:
// Set the cell text
cell.textLabel?.text = "Temp: " + (wdata != nil ? "\((wdata!["current_weather"] as! [String : Any])["temperature"] ?? "---")" : "---")
break
case 1:
// Set the cell text
cell.textLabel?.text = "Elevation: " + (wdata != nil ? "\(wdata!["elevation"] ?? "---")" : "---")
break
case 2:
// Set the cell text
cell.textLabel?.text = "Wind speed: " + (wdata != nil ? "\((wdata!["current_weather"] as! [String : Any])["windspeed"] ?? "---")" : "---")
break
case 3:
// Set the cell text
cell.textLabel?.text = "Feels like: " + (wdata != nil ? "\(((wdata!["daily"] as! [String : Any])["apparent_temperature_max"] as! [NSNumber])[0])" : "---")
break
default:
break
}
// Return cell
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Set the height of cells as fixed
return 60.0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Set the number of rows to 4
return 4
}
// Method that requests the weather data to meteo and updates the wdata property
func requestWeatherData(_ completion: @escaping () -> Void) {
// create the url
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=32.22&longitude=-110.93&daily=temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset¤t_weather=true&temperature_unit=fahrenheit&timezone=America%2FLos_Angeles")!
// now create the URLRequest object using the url object
let request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
// create dataTask using the session object to send data to the server
let task = URLSession.shared.dataTask(with: request, completionHandler: { [self] data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
//create json object from data
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
// Update wdata property
wdata = json
// Call completion handler
completion()
}
} catch let error {
print(error.localizedDescription)
}
})
// Trigger request
task.resume()
}
}
I know it’s a lot of code, but don’t let it intimidate you. Most of it is standard code used on all table view designs, and it’s pretty straightforward.
For now, focus on the @IBOutlet.
Like last time, you need to connect the view from the storyboard to the actual class. You can do this similarly by dragging and dropping with the control button pressed.
Now, rerun your code, and it should show you the weather information like the following:

Now, for the final view, let’s drop a MapView on the last view controller associated with the tab bar and expand it to fit the view edges. Again, don’t forget to set the constraints to match the edges. Otherwise, it won’t keep the size, depending on the device.
Put the following code on the item3.swift class and connect the @IBOutlet accordingly.
//
// ViewController3.swift
// TabBarControllerSample
//
// Created by Juan Mueller on 11/19/22.
// For more, visit /www.ajourneyforwisdom.com
import Foundation
import UIKit
import MapKit
import CoreLocation
class ViewController3: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
// Outlet for mapView
@IBOutlet weak var mapView: MKMapView!
// Location manager property
var locationManager: CLLocationManager?
override func viewDidLoad() {
// Call the viewdidload super
super.viewDidLoad()
// Initialize location manager
locationManager = CLLocationManager()
// Configure location manager to request for location permission
locationManager?.delegate = self
locationManager?.requestAlwaysAuthorization()
locationManager?.requestWhenInUseAuthorization()
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
// Configure map view
mapView.delegate = self
mapView.mapType = .standard
mapView.isZoomEnabled = true
mapView.isScrollEnabled = true
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
// Check if permissions are not available
if manager.authorizationStatus == .denied ||
manager.authorizationStatus == .restricted ||
manager.authorizationStatus == .notDetermined {
return
}
// Trigger location retrieval
locationManager?.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations
locations: [CLLocation]) {
// Initialize coordinate with user location data
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
// initialize default map span and region
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let region = MKCoordinateRegion(center: locValue, span: span)
// Set map region to frame the mapView on a specific region
mapView.setRegion(region, animated: true)
// Initialize map annotation (pin)
let annotation = MKPointAnnotation()
annotation.coordinate = locValue
annotation.title = "You are Here"
// Add annotation on map view
mapView.addAnnotation(annotation)
}
}
Here, you’ll also need to set the permissions to your current location on the info.plist file so that the device requests them on load.
Now rerun your app, and that’s it. You got an interactive map centered on your location!
Customizing the TabBarController
As you’ve seen, working with a TabBarController is pretty straightforward. Adding your views is pretty much a drag-and-drop affair, and ensuring you have your outlets adequately set is easy. And if you want to make further customizations to the tab bar section, well, that’s pretty simple too.
First, to change the text and icon for the pages, you can do so on the corresponding view controller settings.

Secondly, to change the style and format of the tab bar, you can modify the settings in the TabBarController bar itself. For example, you can set the background, font, tint, style, and, most importantly, appearance.
This last setting controls whether the tab bar lives on top of your content (Scroll Edge) or below it (Standard). If it lives on top, the content will be partially visible underneath with transparency when you scroll. However, you can change this, if you prefer.

Customizing the tab bar
Customizing the tab bar is straightforward:
- Change tab titles and icons in each view controller’s settings
- Adjust appearance (background, tint, style) in the UITabBarController
- Control whether the tab bar overlays content or sits below it
UIKit gives you extensive flexibility without requiring much code.
Testing your UITab-Based app
As simple as this example may seem, real-world apps often contain complex navigation paths, asynchronous data loading, and device-specific UI behavior. Testing tab navigation manually can become time-consuming and error-prone as apps scale.
This is where Tricentis Testim Mobile adds significant value. By offering AI-powered mobile test automation with both coded and no-code options, Tricentis Testim Mobile enables teams to validate UI flows—like tab navigation, view switching, and user interactions—across devices without slowing down development.
Moving on
The tab bar remains one of the most reliable and widely used navigation patterns in mobile development—across both iOS and Android. Its simplicity, usability, and familiarity make it a foundational tool every mobile developer should master.
If you want to take your Swift development skills further, understanding UITabBarController is essential. And if you want confidence that your UI behaves correctly in production, pairing solid development practices with Tricentis Testim Mobile ensures your app is ready for real users—without adding testing complexity