Blog

Back to Blog

Working with tabs

Posted on 29 Jul 2013 Written by Alison Clarke

The tabbed view control in ShinobiEssentials enables you to easily create an app where the user can manage which tabs they see (just like a web browser). Here we’re going to create a mock banking app, which displays one bank account per tab. When the user creates a new tab they can choose which account should appear in it.

To get started, you’ll need a copy of Xcode and also (of course) a copy of ShinobiEssentials (you can download a trial here).

Setting things up

First, create a new Single View Application in Xcode. We’ll turn on ARC to make things simpler, but we don’t need Storyboards or Unit testing, and we’ll set the application up for iPad.

Your project will open on the summary page. Go to the “Linked Frameworks and Libraries” section and add QuartzCore.framework and Security.framework. You also need to drag-drop in “ShinobiEssentials.embeddedFramework”, to use both the framework and bundled Resources. To check you did this correctly, open up the project window, go to the Build Phases tab and check that ShinobiEssentials.framework is under “Link Binary With Libraries”, and that under “Copy Bundle Resources”, there are lots of files beginning with “essentials_*.png” (such as “essentials_donebutton.png”).

If you’re using the trial version you’ll need to add your license key. Open up AppDelegate.m, import <ShinobiEssentials/SEssentials.h>, and set the license key inside application:didFinishLaunchingWithOptions: as follows:

#import <ShinobiEssentials/SEssentials.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [SEssentials setLicenseKey:@"your license key"];
    …
}

Now, open up ViewController.h, import <ShinobiEssentials/SEssentialsTabbedView.h>, and define a new variable which we’ll use to store our tabbed view:

#import <UIKit/UIKit.h>
#import <ShinobiEssentials/SEssentialsTabbedView.h>

@interface ViewController : UIViewController
{
    SEssentialsTabbedView *tabbedView;
}
@end

Next, in the viewDidLoad method of ViewController.m, we’ll do some initial setup. First, we create a new SEssentialsTabbedView. We set it up so it has a “new tab” button, and so that the tabs will be a fixed size rather than resizing to fit the text. Next we set the theme to use the Light theme provided by the framework (note that using setSharedTheme will set the theme for all ShinobiEssentials controls), and set the font size for the tab headers. Finally we add the tabbedView to the current view:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
   
    // Create the tabbed view
    tabbedView = [[SEssentialsTabbedView alloc] initWithFrame:self.view.frame];
 
    // Enable the "new tab" button
    tabbedView.hasNewTabButton = YES;
   
    // Set the tabs to a fixed size, rather than resizing when their text changes
    tabbedView.resizeToText = NO;
    
    // Set the theme
    [SEssentialsTheme setSharedTheme:[SEssentialsLightTheme new]];
    // Update the style for the tab headers
    tabbedView.style.defaultFont = [UIFont systemFontOfSize:16];   
 
    // Add the tabbed view to our main view
    [self.view addSubview:tabbedView];
}

If you run the project now, you should see an empty tabbed view, with a new tab button (which doesn’t currently do anything):

01 Empty Tab View

Adding a tab

Let’s add a tab to our empty view. To do this we need an implementation of the SEssentialsTabbedViewDataSource, which is used by the tabbed view to retrieve the view associated with each tab. We’ll make our ViewController class implement the protocol. First, change ViewController.h to declare the protocol:

@interface ViewController : UIViewController<SEssentialsTabbedViewDataSource> 

Now we need to implement the two data source methods: tabbedView:contentForTab, which should return the view associated with the tab passed to it, and tabForTabbedView, which is called when the “new tab” button is pressed, and should create a new tab and associate it with a view. We’ll use a dictionary to store our mapping from tabs to views, so let’s add one as a private instance variable to our ViewController.m class. While we’re editing the top of the file, we’ll also add a #define for some text which we’ll need to use in a couple of places:

#define DEFAULT_TAB_NAME @"Choose an account:"

@interface ViewController ()
{
    @private
    // Dictionary to hold our mappings from tabs to views
    NSMutableDictionary *mapTabToView;
}
@end

Next, we’ll implement tabbedView:contentForTab:, which simply retrieves the view from our dictionary: 

- (UIView *)tabbedView:(SEssentialsTabbedView *)tabbedViewRef contentForTab:(SEssentialsTab *)tab
{
    // Just get the view out of our dictionary
    return [mapTabToView objectForKey:[NSValue valueWithNonretainedObject:tab]];
} 

Now we’ll add a method to create a default view (which will be empty for now):

- (UIView *)createDefaultView
{
    // Create a view to display in the tab
    UIView *defaultView = [[UIView alloc] initWithFrame:[tabbedView contentViewBounds]];
    return defaultView;
}

We now implement tabForTabbedView:, which creates a tab that displays the text “Choose an account:”, creates a default view, and stores the tab and view in our dictionary:

- (SEssentialsTab *)tabForTabbedView:(SEssentialsTabbedView *)tabbedViewRef
{
    // Create a new tab
    SEssentialsTab *myTab = [[SEssentialsTab alloc] initWithName:DEFAULT_TAB_NAME icon:nil];
    
    // Create a default view to display in the tab
    UIView *defaultView = [self createDefaultView];
    
    // Add the view and tab to our dictionary
    [mapTabToView setObject:defaultView forKey:[NSValue valueWithNonretainedObject:myTab]];
    
    // Return the tab
    return myTab;
}

And finally, in viewDidLoad, we need to tell our tabbedView where its datasource is, make sure the dictionary is initialized, and add a single tab to start with, which will also use our tabForTabbedView method to get its tab: 

- (void)viewDidLoad
{
    …
    // Set its datasource to be ourself
    tabbedView.dataSource = self;
 
    // Set up the Tab=>View dictionary
    mapTabToView = [[NSMutableDictionary alloc] init];
 
    // Add a tab to start with
    [tabbedView addTab:[self tabForTabbedView:tabbedView]];
 
    // Add the tabbed view to our main view
    [self.view addSubview:tabbedView];
}

If you run the application now, you should see a tab in the tab bar, with an empty view below it:

02 Added Empty Tab 

If you press the “new tab” button, another (identical) tab should appear, and you can drag the tabs around, and create as many tabs as you like. When the tabs fill the screen, an “overflow dropdown” button will appear which displays a list of all the tabs and allows you to choose one.

So, we’ve made an application that can display lots of empty screens: now we need some data to display in them. 

Adding some data

First we’ll create a simple data class to store the details of an individual bank account. Create a new Objective-C class in your project, called “BankAccount”. Copy the following code into your .h file:

#import <Foundation/Foundation.h>

@interface BankAccount : NSObject
@property (assign, readonly) NSString *name;
@property (assign, readonly) int accountNumber;
@property (assign, readonly) int routingNumber;
@property (assign, readonly) double balance;
 
- (id)initWithName:(NSString*)name accountNumber:(int)accountNumber routingNumber:(int)routingNumber balance:(double)balance;

@end

and the following code into your .m file:

#import "BankAccount.h"

@implementation BankAccount

- (id)initWithName:(NSString*)name accountNumber:(int)accountNumber routingNumber:(int)routingNumber balance:(double)balance
{
    self = [super init];
    if (self) {
        _name = name;
        _accountNumber = accountNumber;
        _routingNumber = routingNumber;
        _balance = balance;
    }
    return self;
}
 
- (NSString *)description {
    // Return a string which will lay out the account details using tabs - this is just for 
    // simplicity of displaying the account details in the UI for the purposes of the demo app
    return [NSString stringWithFormat: @"Account Name:\t\t\t\t%@\n\nAccount Number:\t\t\t\t%d\n\nRouting Number:\t\t\t\t%d\n\nBalance:\t\t\t\t\t\t$%.02f", _name, _accountNumber, _routingNumber, _balance];
}

@end

This should be fairly self explanatory: it’s just a class with properties for the name, account number, sort code and balance, with an initializer to set all the properties in one go, plus an override of description which gives us a nicely formatted string which we can use later on to display the account details. (Obviously in a real banking app you probably wouldn’t be using primitive types for storing these details, but it keeps things simpler for now.)

Next, let’s create a class to hold a collection of BankAccount objects. Create a new Objective-C class in your project, called “BankAccountDataSource” (for reasons which will become apparent later). Copy the following code into your .h file: 

#import "BankAccount.h" 

@interface BankAccountDataSource : NSObject
 
/** Add an account to the list */
- (void)addAccount:(BankAccount*)account;

/** Retrieve an account from the list */
- (BankAccount*)accountAtIndex:(NSInteger)index;

@end

and the following into your .m file: 

@interface BankAccountDataSource ()
{
    @private
    NSMutableArray *accounts;
}
@end
 
@implementation BankAccountDataSource
 
- (id)init
{
    self = [super init];
    if (self)
    {
        accounts = [[NSMutableArray alloc] init];
    }
    return self;         
}  
 
- (void)addAccount:(BankAccount *)account
{
    [accounts addObject:account];
}
 
- (BankAccount*)accountAtIndex:(NSInteger)index
{
    return (BankAccount*)[accounts objectAtIndex:index];
} 

@end

Again, hopefully this is fairly self-explanatory: the new class just holds an array of BankAccount objects, which is created in the initializer, and provides methods to add an account to the array, and retrieve an account at a certain point in the array.

We’ll need to use a BankAccountDataSource from our ViewController, so in ViewController.m, add the following code to create one and populate it with some dummy data: 

#import "BankAccount.h"
#import "BankAccountDataSource.h"

@interface ViewController ()
{
    @private
    …
    // Source for bank account data
    BankAccountDataSource *dataSource;
}
 
@implementation ViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
   
    // Create and set up a bank account data source
    dataSource = [[BankAccountDataSource alloc] init];
    [dataSource addAccount:[[BankAccount alloc] initWithName:@"My Checking Account" accountNumber:12345678 routingNumber:123456789 balance:394.27]];
    [dataSource addAccount:[[BankAccount alloc] initWithName:@"Joint Checking Account" accountNumber:38472363 routingNumber:352483956 balance:52.39]];
    [dataSource addAccount:[[BankAccount alloc] initWithName:@"Savings Account" accountNumber:28361234 routingNumber:392756386 balance:8364.28]];
    [dataSource addAccount:[[BankAccount alloc] initWithName:@"Mortgage" accountNumber:98342563 routingNumber:243138287 balance:-163872.36]];
    …
}

We’d like our default view to display a list of bank accounts,  so the user can select which one they want to display in the tab. We’ll use a UITableView to display the accounts (one per row), which requires a data source that implements the UITableViewDataSource protocol to provide the data for each row (the account names).

As our bank account data is stored in our BankAccountDataSource class, we’ll turn that into a UITableViewDataSource. First, add the protocol to the .h file:

@interface BankAccountDataSource : NSObject<UITableViewDataSource>

Now add the following two required methods to BankAccountDataSource.m:

#pragma mark - Table view data source

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [accounts count];
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
   
    // Set the text to the name of the bank account at this index
    cell.textLabel.text = ((BankAccount*)[accounts objectAtIndex:indexPath.row]).name;
    cell.textLabel.textColor = [UIColor darkGrayColor];
   
    return cell;
}

tableView:numberOfRowsInSection tells the table view how many rows there are in the given section. We’ll only have one section in our table view, so our implementation just returns the total number of bank accounts.

tableView:cellForRowAtIndexPath: returns a cell to display at the given location. We use the standard pattern to create or retrieve a UITableViewCell with the default style, then set its text to be the name of the bank account at the given index.

(I haven’t gone into much detail about the UITableView methods here as it’s not the main point of this tutorial, but if you want to know more, have a look at Creating a Table View Programmatically in Apple’s Table View Programming Guide.) 

We want to create our table view of accounts in our default tab, so we modify the createDefaultView method: 

- (UIView *)createDefaultView
{
    // Create a view to display in the tab
    UIView *defaultView = [[UIView alloc] initWithFrame:[tabbedView contentViewBounds]];
    
    // Create a table view to display the different account names
    UITableView *defaultTableView = [[UITableView alloc] initWithFrame:[tabbedView contentViewBounds] style:UITableViewStylePlain];
    defaultTableView.dataSource = dataSource;
    
    // Add the table view to the main view
    [defaultView addSubview:defaultTableView];
    
    return defaultView;
}

If you run the application now, it should display a list of our bank accounts in the new tab:

03 Added Account List

Putting it all together

Now we need to make the application display the account details in the current tab when the user selects an account from the list. To do this, we need a delegate which implements the UITableViewDelegate protocol. We’ll use our ViewController class for this, as we’ll need to do some manipulation of the tabbed view. Add the protocol to the .h file: 

@interface ViewController : UIViewController<SEssentialsTabbedViewDataSource, UITableViewDelegate>

And set the delegate in createDefaultView in the .m file:

- (UIView *)createDefaultView
{
    ...
    defaultTableView.dataSource = dataSource;
    defaultTableView.delegate = self;
    ...
}

Finally we need to implement the delegate method tableView:didSelectRowAtIndexPath: in the ViewController.m file, to display the account details in the current tab: 

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Create a new text view based on the selected account
    BankAccount* account = [dataSource accountAtIndex:indexPath.row];
    UITextView* newView = [[UITextView alloc] initWithFrame:CGRectMake(30, 20, tableView.bounds.size.height-60, tableView.bounds.size.width-40)];
    newView.font = [UIFont systemFontOfSize:18];
    newView.textColor = [SEssentialsTheme sharedTheme].primaryTextColor;
    [newView setEditable:NO];
    [newView setText:[account description]];
   
    // Replace the table view with our new view
    UIView *parentView = tableView.superview;
    [parentView addSubview:newView];
    [tableView removeFromSuperview];   
   
    // Change the name of the current tab
    SEssentialsTab *tab = tabbedView.activeTab;
    tab.name = account.name;
    // Call updateView on the tab's header view to update its name in the display
    [tab.tabHeaderView updateView];
}

The first part of this grabs the account that’s displayed in the row selected by the user. It then creates a new text view, sets its font and text color, and makes sure the user can’t edit the text. Then it uses the account’s description as the text to display.

Next, we grab the tableView’s parent (which is the main view for the tab), and replace the table view with our new view. 

We then need to update the tab name to match the account name. The tab we’re changing is the one that’s currently active, so we can easily grab it from our tabbedView object. We change its name, then we need to call updateView on the tab’s header view, to tell it that the tab has changed and the header needs to be updated.

If you run the app now, you should be able to select an account and its details will be displayed. You can then open up new tabs to display different bank accounts.

04 Finished App

Finishing Touches

Before we finish, there’s one more feature we could add to improve the user’s experience. Currently you can close all but the last tab, so there’s no way of getting rid of those last account details. It would be nice if when you hit the close icon on the last tab, the tab reverted to the “Select an account” view.

We can do this by implementing SEssentialsTabbedViewDelegate, which provides hooks into various user actions associated with the tabs. First, we’ll add the protocol to the .h file:

@interface ViewController : UIViewController<SEssentialsTabbedViewDataSource, UITableViewDelegate, SEssentialsTabbedViewDelegate>

Next we need to tell the tabbed view where its delegate is:

- (void)viewDidLoad
{
    ...
    // Set its datasource and delegate to be ourself
    tabbedView.dataSource = self;
    tabbedView.delegate = self;
    ...
}

The delegate method we need to implement is tabbedView:shouldRemoveTab:, which gets called before a tab is removed. We need to check whether the tab to remove is the last one, and if it is, reset it to the default state, and return NO, so the tab isn’t removed; for all other tabs we just return YES.

#pragma mark - Tabbed view delegate

- (BOOL)tabbedView:(SEssentialsTabbedView *)tabbedViewRef shouldRemoveTab:(SEssentialsTab *)tab
{
    // If this is the last tab, reset it instead of removing it
    if ([tabbedViewRef.allTabs count] == 1)
    {
        [self resetTab:tab];
        return NO;
    }
    else
    {
        return YES;
    }
}

The resetTab: method needs to create a default view, update the mapping, update the tab name, and re-render the tab:

- (void)resetTab:(SEssentialsTab *)tab
{
    // Create a default view
    UIView *defaultView = [self createDefaultView];
    // Update our mapping
    [mapTabToView setObject:defaultView forKey:[NSValue valueWithNonretainedObject:tab]];
    // Update the tab name
    tab.name = DEFAULT_TAB_NAME;
    [tab.tabHeaderView updateView];
    // Call activateTab to re-render the tab's contents
    [tabbedView activateTab:tab];
}

If you run the app now, when you hit close on the last tab, it will revert to the “Choose an account” view.

And there we have it…Our very own “banking app”, allowing a user to display whichever accounts they want and flick between them.

If you got stuck with any of this, you can browse the finished project on GitHub, or download the zip.

Back to Blog