UITabbar: Comparing storyboards vs programmatic setup



After watching the course by Sean Allen about doing your UI 100% programmatically I was inspired to write a small blog post about the two ways to implement a UITabBar.

I will start with the storyboards and then show the programmatic way. Sometimes the storyboards method feels easier especially at the beginning perhaps, but when it comes to change the UI later, the programmatic way is much more flexible. At least I recommend to try both!

Doing your UI With StoryBoards

Storyboards are great, but they sure take up a lot of space!

First create a new Project in Xcode. Look for Tabbed App.

Your storyboard will look like this:

It is ready to be edited. Run the project on the simulator and try it out. Both views will look like this out of the box:

The Tabbar Items can be customised easily in the inspector on the right:

Doing your UI programmatically: Main setup

First create a new Project in Xcode. Select on Storyboards and not SwiftUI.

Delete the `main.storyboard' file.

When in the project remove the references to the main storyboard in two places:

Making sure to hit the enter key:

In the General setting tab (press Enter when empty) and in the plist file.

First steps

In iOS 13 Apple decided to split the AppDelegate.swift file in two into an AppDelegate.swift and a SceneDelegate.swift. This allows for multi-windows operation. Before you had UIWindow and a window will be diaplayed. Now you have scenes. This allows to have two screens of the same app to be diswplayed at the same time, ex iPad. There is still one AppDelegate but now multiple scenes.

Inside SceneDelegate.swift we have a window property. It used to be in AppDelegate.swift. By default SceneDelegate.swift gives us this hint already about the window property:

In SceneDelegate.swift we need to specify our windowScene property in the function scene(willConnectTo:) which is the first function to run :

// we give a name to our variable
guard let windowScene = (scene as? UIWindowScene) else { return }
// our window will be the whole screen
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
//assign windowScene to the windowScene property of our window 
window?.windowScene = windowScene
// assign the root controller to the window
window?.rootViewController = ViewController()
// and make it visible at start
window?.makeKeyAndVisible()

We can test it in our ViewController.swift file using view.backgroundColor = .systemGreen in viewDidLoad()

class ViewController: UIViewController {
    override func viewDidLoad() {
    super.viewDidLoad()
        view.backgroundColor = .systemGreen
    }
}

Setup the tabbar.

The tabbar will hold our two navigation controllers.
Delete the file ViewController.swift and edit the SceneDelegate.swift .
Add:

window?.rootViewController = UITabBarController()

Make two new cocoa class files subclassing UIViewController and call them SearchVC and FavoritesListVC for example. As a functionality test we add a background color to their views. Feel free to choose a color:

     // incidentally this supports the darkmode automatically!
        view.backgroundColor = .systemPink

We now create two navigation controllers for our tabbar, which will hold an array of NavigationControllers (in our case two) and each of them will have an array of ViewControllers (in this case just one).

Edit the SceneDelegate.swift as follows:

// create the nav controllers
let searchNC = UINavigationController(rootViewController: SearchVC())
let favoritesNC = UINavigationController(rootViewController: FavoriteListyVC())
// create tabbar with array of nav controllers
let tabbar = UITabBarController()
tabbar.viewControllers = [searchNC, favoritesNC]

Run the project, you should be able to select two screens, with no buttons. Still you will be able to tab between the two.

Customize and refactor

Now we will customize the two Navigation Controllers and the tabbar.

func createSearchNavigationController() -> UINavigationController {
    // we create the view controller and insert into the nav controller and return
    let searchVC = SearchVC()
    searchVC.title = "Search"
    // it is a system tabbaritem , tag zero because it is the first one
    searchVC.tabBarItem = UITabBarItem(tabBarSystemItem: .search, tag: 0)
    return UINavigationController(rootViewController: searchVC)
}

func createFavoritesNavigationController() -> UINavigationController {
    let favoritesListVC = FavoritesListVC()
    favoritesListVC.title = "Favorites"
    // it is a system tabbaritem , tag zero because it is the first one
    favoritesListVC.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 1)
    return UINavigationController(rootViewController: favoritesListVC)
}

Refactor into a tabbar function

func createtabbar() -> UITabBarController {
    let tabbar = UITabBarController()
    // here we assign a tint to all our tabbars, this will be visible on the items (icons)
    UITabBar.appearance().tintColor = .systemPink
    // replace our array variables with the functions we created
    tabbar.viewControllers = [createSearchNavigationController(), createFavoritesNavigationController()]
    return tabbar
}

Our scene function will be much cleaner and now look like:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }

    window = UIWindow(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
    window?.rootViewController = createtabbar()
    window?.makeKeyAndVisible()
}

Otherwise you can create a separate file with a tabbar class inheriting from UITabBarController and containing the functions createSearchNavigationController() , createFavoritesNavigationController() and initialising the tab bar.

Now running in the simulator you should be able to tab between these two:

Custom Tabbar Items

As per Swift5.1 and Xcode the title and image of the system tab bar items cannot be changed. You cannot use the Apple icon and use a different title. They are defined as enum in obj c as follow. This is the list of the system options:

typedef enum UITabBarSystemItem : NSInteger {
    case more, favorites, featured, topRated, recents, contacts, 
    history, bookmarks, search, downloads ,mostRecent, mostViewed
} UITabBarSystemItem;

This has a reason. Some icons are immediately recognisable across the whole iOS ecosystem and will have the same meaning to everyone, therefore it is not possible to use them differently than what for they were intended to. Also in this way Apple creates a better user experience across all its apps.

To use a custom icon and title select an it from in the storyboards like this:

itemWithStoryboards

The SF Symbols introduced in WWDC2019 are a great option. Download SF Symbols from the Apple website. Then open the app and select a symbol you like. Copy the name with CMD-Shift-C. Assign an icon and text to a Tabbar Item using SF symbols like "person.fill" for the icon ane "Profile" for title as below:

func createProfileNC() -> UINavigationController {
    let profileVC = ProfileVC()
    profileVC.title = "My Profile"
    profileVC.tabBarItem = UITabBarItem(title: "Profile", image: UIImage(systemName: "person.fill"), tag: 2)
    return UINavigationController(rootViewController: profileVC)
}