Advertisement

Exploring Tab Bar Controller

by

While navigation controllers let users navigate hierarchical content or complex data by managing a stack of view controllers, tab bar controllers manage an array of view controllers that don't necessarily have a relation to one another. In this article, we will explore tab bar controllers in more detail by creating a tabbed application from scratch.


Introduction

The UITabBarController class is another UIViewController subclass. While navigation controllers manage a stack of related view controllers, tab bar controllers manage an array of view controllers that have no explicit relation with one another.

The Clock and Music applications on iOS are two prime examples of tab bar controllers. Just like any other UIViewController subclass, a tab bar controller manages a UIView instance.

The view of a tab bar controller is composed of two subviews:

  • the tab bar at the bottom of the view
  • the view of one of the view controllers the tab bar controller manages

Before We Start

There are a few caveats to be aware of when working with tab bar controllers. Even though instances of UITabBar can only display five tabs, the UITabBarController class can manage more view controllers. Whenever a tab bar controller manages more than five view controllers, the tab bar's last tab is titled More.

The additional view controllers can be accessed via this tab and it is even possible to edit the position of the tabs in the tab bar.

Although tab bar controllers manage a view, your application is not supposed to directly interact with a tab bar controller's view.

If you decide that a tab bar controller is the right choice for an application, the tab bar controller is required to be the root view controller of the application window. In other words, the root view of the application window is always the tab bar controller's view. A tab bar controller should never be installed as a child of another view controller. This is one of the key differences with navigation controllers.


Tabbed Library

In this article, we revisit the Library application that we built in the previous article. By doing so, we can reuse several classes and get up to speed faster. In addition, it will show you that navigation controllers and tab bar controllers are quite different and that they are used in different situations and use cases.

The application that we will build in this lesson will be named Tabbed Library and will be based on the UITabBarController class. While we build the Tabbed Library application, you'll notice that the use of a tab bar controller forces the application in a very specific user interface paradigm that allows for little flexibility. Tab bar controllers are incredibly useful, but you have to accept that they put constraints on your application's user interface to some extent.

Open Xcode, create a new project (File > New > Project...), and select the Empty Application template.

Name the project Tabbed Library, assign an organization name and company identifier, and set Devices to iPhone. Tell Xcode where you want to save the project and hit Create.

Even though Xcode includes a Tabbed Application template, I prefer to start with an empty application template so you understand how the various pieces of the puzzle fit together. You'll notice that tab bar controllers aren't that complicated.


Taking a Head Start

When the Tabbed Library application is finished, the tab bar controller of the application will manage six view controllers. Instead of creating every view controller class from scratch, we are going to cheat a little bit by reusing the view controller classes that we created in the previous article.

In addition, we'll create several instances of the same view controller class to save us some time. The goal of this article is not to create a bunch of view controller classes. At this point, you should be pretty familiar with how that works.

Download the source code from the previous article and open the Xcode project that's included in the source files in a new Finder window. Find the TSPAuthorsViewControllerTSPBooksViewController, and TSPBookCoverViewController classes and drag them to your new project. Make sure to copy the files into the new project by checking the checkbox labeled Copy items into destination group's folder (if needed) and don't forget to add the files to the Tabbed Library target.

In addition to these three classes, we also need to copy the folder of resources, containing Books.plist and the image files, into our new project. Drag the folder named Resources into our project and use the same settings that we used to copy the class files. We're now ready to instantiate the application's tab bar controller and populate it with its first view controller.


Adding a Tab Bar Controller

Adding a tab bar controller is easy. Open the project's main storyboard, Main.storyboard. Wait a minute. Where's my storyboard? Because we opted for the Empty Application template, Xcode didn't give us a storyboard.

Adding a Storyboard

Select New > File... from the File menu and choose Storyboard from the iOS User Interface category on the left.

Set Device Family to iPhone and name the storyboard Main. There's no need to add the .storyboard extension. Xcode will add that for you.

We also have to tell Xcode that it needs to use Main.storyboard as the main interface of the application. Select the project in the Project Navigator, choose the Tabbed Library target from the list of targets, and set Main Interface to Main or Main.storyboard.

Before we start working with the storyboard, we need to update the current implementation of application:didFinishLaunchingWithOptions: in the TSPAppDelegate class. Open TSPAppDelegate.m and update the implementation as shown below.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    return YES;
}

You can create iOS applications without storyboards, but that means that you need to explicitly instantiate the window of your application, which is what you saw in the implementation of application:didFinishLaunchingWithOptions:. When working with storyboards, however, you simply return YES and the storyboard does the rest.

Adding a Tab Bar Controller

Open Main.storyboard and drag a UITabBarController instance from the Object Library on the right. By default, the tab bar controller comes with two view controllers. However, I want to illustrate how you can add view controllers to the tab bar controller manually so select the view controllers—not the tab bar controller—and delete them from the storyboard.

If you run the application in the iOS Simulator, you should a tab bar at the bottom and a black background. This may seem unimportant, but it shows how the tab bar controller works. The tab bar controller manages an array of view controllers, similar to how a navigation controller manages a stack of view controllers.

What we need to do is add a few view controllers to the storyboard and add them to the viewControllers property of the tab bar controller. Let's see how this works.

Adding View Controllers

Drag a UITableViewController instance from the Object Library to the workspace and set its class to TSPAuthorsViewController in the Identity Inspector. Select the view controller's table view and set the number of Prototype Cells to 0 in the Attributes Inspector as we did in the previous tutorial.

To add the authors view controller to the tab bar controller's array of view controllers, drag from the tab bar controller to the authors view controller while holding the Control key. Select view controllersunder the Relationship Segue category—from the menu that appears.

A tab bar controller with one tab isn't that useful so let's add another view controller to the mix. Drag another UITableViewController instance from the Object Library, set its class to TSPBooksViewController, and set the number of Prototype Cells to 0. Create the relationship segue as we did for the authors view controller.

Add a UIViewController instance to the workspace and set its class to TSPBookCoverViewController in the Identity Inspector. Add a UIImageView instance to the view controller, as we did in the previous article, and connect it with the bookCoverView outlet of the view controller. Create the relationship segue with the tab bar controller as we did for the table view controllers.

You can only create segues when the workspace is not zoomed out, which can be an issue at times. However, you can also create connections, such as segues, in the navigator on the left. This is often much easier and less clunky.

Build and run the application. At this point, the tab bar contains three tabs. Tapping a tab shows the view controller associated with the tab.

Have you tried tapping the name of an author in the authors view controller? And why is the books view controller showing us an empty table view? It's time for some debugging.

Fixing the Authors View Controller

When you tap an author's name in the authors view controller, the application crashes. The first thing you should do when you're faced with a crash is inspecting Xcode's console. This is what it tells me:

2014-03-27 12:42:07.964 Tabbed Library[1943:60b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Receiver (<TSPAuthorsViewController: 0x8ca22e0>) has no segue with identifier 'BooksViewController''

The message that Xcode displays in the console is not difficult to decipher. We tell the view controller to perform a segue with identifier BooksViewController, but that segue doesn't exist. The result is a crash.

This bug is easy to fix by embedding the authors view controller in a navigation controller, creating a segue to an instance of the TSPBooksViewController class, and naming the segue BooksViewController. I leave that up to you as an assignment since we already did this in the previous article.

The more important issue in this example is understanding that the three view controllers that are linked to the tab bar controller don't communicate with one another. The idea behind a tab bar controller and a navigation controller is very different. A navigation controller maintains an array of view controllers, the navigation stack. The view controllers in the navigation stack have an implicit relation with one another in the sense that they are part of the same navigation stack.

A tab bar controller also manages an array of view controllers, but the view controllers don't know about each other. They cannot communicate with each other through the tab bar controller. In the previous article, we passed data from the authors view controller to the books view controller when an author as tapped. This pattern isn't applicable in a tab bar controller. Of course, we could implement a solution to show the user the books view controller when an author is tapped in the authors view  controller, but it's important that you understand that that isn't the goal of a tab bar controller.

The Clock and Music applications on iOS are good examples of how a tab bar controller should be used. The view controllers that the tab bar controller manages in the Music application have no relation to one another apart from the fact that they show songs.

Before you continue, make sure that you understand the concepts of a navigation controller and a tab bar controller as they are important later in this series.


Adding Another Table View Controller

Let's add a fourth view controller to the tab bar controller that shows every book in Books.plist. Create a new UITableViewController subclass and name it TSPAllBooksViewController.

This view controller will extract all the books from Books.plist and display them alphabetically in a table view. Open TSPAllBooksViewController.m and add a new property books of type NSArray to the class extension at the top.

#import "TSPAllBooksViewController.h"

@interface TSPAllBooksViewController ()

@property NSArray *books;

@end

In the view controller's viewDidLoad method, we invoke extractBooks, a helper method that we'll implement shortly. I generally try to keep viewDidLoad as concise as possible by wrapping tasks in helper methods like extractBooks. This makes the code easier to read and more maintainable.

- (void)viewDidLoad {
    [super viewDidLoad];

    // Set Title
    self.title = @"Books";

    // Extract Books
    self.books = [self extractBooks];
}

Let's inspect the implementation of extractBooks. We start by creating a mutable array to which we'll be adding the books of each author in the property list. The next two lines should be familiar if you've read the previous article. We ask the application bundle for the file path of Books.plist and use it to load the contents of Books.plist into an array named authors. We then iterate over the array of authors and add the books of each author to the mutable array we created earlier. To sort the array of books, we create a sort descriptor with a key of Title. After we sort the books by title, a new array is created, result, by sorting the mutable array using the sort descriptor. We return the sorted list of books.

- (NSArray *)extractBooks {
    // Buffer
    NSMutableArray *buffer = [[NSMutableArray alloc] init];

    // Load Authors
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Books" ofType:@"plist"];
    NSArray *authors = [NSArray arrayWithContentsOfFile:filePath];

    for (int i = 0; i < [authors count]; i++) {
        NSDictionary *author = [authors objectAtIndex:i];

        // Add Books to Buffer
        [buffer addObjectsFromArray:[author objectForKey:@"Books"]];
    }

    // Sort Books Alphabetically
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"Title" ascending:YES];
    NSArray *result = [buffer sortedArrayUsingDescriptors:@[sortDescriptor]];

    return result;
}

Sort descriptors, instances of NSSortDescriptor, are used to sort collections of objects, such as arrays, by specifying the property that needs to be used to compare two objects of the collection. You can safely ignore this portion of the implementation of extractBooks if it isn't entirely clear,  because it isn't important in the scope of this lesson.

The implementations of the UITableViewDataSource protocol method is very similar to what we saw earlier in this series. Take a moment to inspect the implementation of each method below.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.books count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // Dequeue Reusable Cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Fetch Book
    NSDictionary *book = [self.books objectAtIndex:[indexPath row]];
    
    // Configure Cell
    [cell.textLabel setText:[book objectForKey:@"Title"]];
    
    return cell;
}

As we saw in the lesson on table views, we need to declare cell reuse identifier and we also need to register a class for the cell reuse identifier.

@implementation TSPAllBooksViewController

static NSString *CellIdentifier = @"Cell Identifier";
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Set Title
    self.title = @"Books";
    
    // Extract Books
    self.books = [self extractBooks];
    
    // Register Class for Cell Reuse
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
}

With the new view controller class ready to use, head back to the storyboard, drag a UITableViewController instance from the Object Library, and set its class to TSPAllBooksViewController. With the table view selected, set the number of Prototype Cells to 0 in the Attributes Inspector. Before you run the application in the iOS Simulator, create the relationship segue between the tab bar controller and the table view controller we just added.

Build and run the project to see the result of our hard work. If you have an eye for detail, you may have noticed that the title of the fourth tab only appears after the tab is selected. Can you guess why that is?


View Did ... Not Load

The cause of this odd quirk is actually quite simple. In general, a view isn't loaded into memory until it's absolutely necessary. This usually means that a view is loaded into memory when it's about to be shown to the user.

When the Tabbed Library application is launched, the first tab is selected by default. As long as the fourth tab isn't selected—by the user or programmatically—there's no need to load the view of the fourth view controller. As a result, the viewDidLoad method is not called until the fourth tab is selected, which in turn means that the title is not set until the fourth tab is selected.

The solution is simple. A better approach is to set the view controller's title when the view controller is initialized. If we set the title in the class's init method, we can be confident that the view controller's title is set in time.

Open TSPAllBooksViewController.m and add a method named initWithCoder: as shown below. This method is invoked by the operating system to create an instance of the class.

- (id)initWithStyle:(UITableViewStyle)style {
    self = [super initWithStyle:style];
    
    if (self) {
        // Set Title
        self.title = @"Books";
    }
    
    return self;
}

This is also a good time to inspect the flow of a typical init method. An initialization method typically starts with a call to the init method of the superclass, in this caseinitWithCoder:. I emphasized why this is important when we discussed theviewDidLoad method earlier in this series. The result of [super initWithCoder:aDecoder] is assigned to self, the instance of the class we're working with.

In the next step, we verify that self isn't nil. If self is set—not nil—it's time to further configure the instance of the class, self. An initialization method should always return either the class instance or nil if something has gone wrong.

In the if statement in initWithCoder: we set the view controller's title property to solve the problem I discussed a moment ago.

That was a lot of theory to fix a trivial problem. However, the above explanation is a good introduction to another important element of tab bar controllers, tab bar items. At the moment, the tabs of the tab bar only show the title of each view controller or, in the case of the second and third vier controllers, a default title of Item. In most applications, the tabs also show a small icon hinting at the function or purpose of the view controller underneath the tab. Let's see how to implement this.


Tab Bar Items

In the previous lesson, I wrote that every view controller on a navigation stack keeps a reference to the navigation controller managing the stack. The same is true for view controllers that are managed by a tab bar controller. A view controller managed by a tab bar controller keeps a reference to the tab bar controller in its tabBarController property.

In addition to these properties, a view controller also has a tabBarItem property, a unique instance of the UITabBarItem class. This property is used when the view controller is a child of a tab bar controller. A tab bar item has a title, an image, and a tag. The tag is just an integer that can be used to identify a tab bar item in the tab bar. By default, the tab bar item's title is inherited from the view controller's title property, which is why the first and last tab in the Tabbed Library application bear the title of their respective view controller.

Open TSPAuthorsViewController.m and add a method named initWithCoder: as shown below.

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    
    if (self) {
        // Set Title
        self.title = @"Authors";
        
        // Set Tab Bar Item
        self.tabBarItem = [[UITabBarItem alloc] initWithTitle:@"Authors" image:[UIImage imageNamed:@"icon-authors"] tag:0];
    }
    
    return self;
}

We create a tab bar item and assign it to the view controller's tabBarItem property. The initializer of the UITabBarItem class accepts a title (NSString), an image (UIImage), and a tag (NSInteger). Before building and running the project, download the source files of this lesson and drag icon-authors.png and icon-authors@2x.png into your project. As you might remember, the file with the @2x suffix targets devices with a retina display, whereas the file without the @2x suffix targets non-retina devices.

Note that it isn't necessary to specify the file extension of the image file when using the class method imageNamed: of UIImage. In general, you don't need to specify what version of the file—retina or non-retina—to use. Based on the file name and the device's hardware, the operating system decides which version it uses.

I've also moved the title assignment to the initWithCoder: method as we did in the TSPAllBooksViewController class. Run the application once more to see the result.

We can do the same for the TSPAllBooksViewController class. Open MTAllBooksViewController.m and update the initWithCoder: method as shown below.

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    
    if (self) {
        // Set Title
        self.title = @"Books";
        
        // Set Tab Bar Item
        self.tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:UITabBarSystemItemContacts tag:1];
        
        // Set Badge Value
        [self.tabBarItem setBadgeValue:@"12"];
    }
    
    return self;
}

In addition to setting the view controller's title, we set its tabBarItem property. However, this time we make use of initWithTabBarSystemItem:tag: to configure the tab bar item. You can use this method if you wish to use a system supplied tab bar item. The first argument of this method, UITabBarSystemItem, determines both the title and the image of the tab bar item.

It's also possible to give a tab bar item a badge value as shown in the above implementation of initWithCoder:. The badge value is expected to be an NSString instance.

When working with tab bar controllers, keep in mind that it's the root view controller of each tab that determines how the tab bar item of the respective tab looks. For example, if a tab bar controller manages a navigation controller with a number of view controllers, it's the tab bar item of the navigation controller's root view controller that's used by the tab bar controller's tab bar. The UITabBarItem class has a few other methods to further customize the look and feel of a tab bar item.


More View Controllers

Before ending this article, I'd like to show you how the tab bar looks when the tab bar controller manages more than five view controllers. As I mentioned earlier, only five tabs are displayed at any one time, but the tab bar controller provides support for managing more than five child view controllers.

Open the main storyboard and add two more instances of UITableViewController. Create a relationship segue for each table view controller and run the application in the iOS Simulator to see the result.

The extra view controllers that we added aren't very useful, but they show how a tab bar controller manages more than five child view controllers. The tab bar controllers gives access to the fifth and sixth view controller of the tab bar controller. The user's even given the ability to edit the positions of the view controllers in the tab bar.


Conclusion

It's important to understand that the UITabBarController and UINavigationController classes each represent a unique user interface paradigm. This article also shows that tab bar controllers aren't difficult to master once you understand the components involved.

In the next article, we'll take a look at data persistence on iOS and the options you have as a developer.

Advertisement