Wednesday 4 January 2012

UITableView introduction

I've been playing with UITableView for a while so I've decided to write this tutorial in which I will cover everything I've used so far, from the basic data source to the the custom table cell. For the final goal of this tutorial I will use my favorite task management app - Wunderlist which is practically based on UITableView and it can serve as a great example! It's a great app and I recommend it to all! Code bellow is already on Github. So lets start…

Unlike in Android or JME, where you specify what to display and how, in iPhone you have the UITableView whose UI is ready, you only have to specify data source and application logic. It's that easy! This is done by using the UITableViewController. UITableViewController is a subclass of UIViewController and handles everything from the user interaction to the data source for our UITableView. It's view is always UITableView. Lets start our project and create one…


Create a plain subclass of UIViewController (without the XIB) and name it TasksViewController. Change the header file, for controller to extend UITableViewController:

@interface TasksViewController : UITableViewController

Now we have to display this controller and its view. We do this in the app delegate file. (ignore closed uiapplicationdelegate tag)

#import "TasksViewController.h"

@interface AppDelegate : UIResponder 
{
    TasksViewController *tasksViewController;
}

and use it in the applicationdidFinishLaunchingWithOptions method of the implementation(.m) file:

tasksViewController = [[TasksViewController alloc] init];
    
[self.window setRootViewController:tasksViewController];
[self.window makeKeyAndVisible];

Now when you run the app, you should get something like this


We'll that's impressive. We have a fully functional … blank table.
We have to have things to fill our table so we will have to write a model to be used as a datasource for our table. I will create a class Task with basic attributes like number, description, dateCreated, highPriority and finished.
You can ignore following lines of code since goal of this tutorial is to learn UITableView, not the models in Objective-C.

#import 

@interface Task : NSObject
{   
    NSString *taskId;
    NSString *description;
    NSDate *dateCreated;
    BOOL highPriority;
    BOOL finished;
}

- (id)initWithDescription:(NSString *)desc;

@property (nonatomic,retain)  NSString *description; 
@property (nonatomic,retain)  NSString *taskId;
@property (nonatomic,retain)  NSDate *dateCreated;
@property (nonatomic) BOOL highPriority;
@property (nonatomic) BOOL finished;

@end


#import "Task.h"

@implementation Task

@synthesize taskId, description, dateCreated, highPriority, finished;

-(id) initWithDescription:(NSString *)desc
{
    self = [super init];
    
    if (self) {
        self.taskId = [NSString stringWithFormat:@"%c%c%c%c%c",
                       '0' + rand() % 10,
                       'A' + rand() % 26,
                       '0' + rand() % 10,
                       'A' + rand() % 26,
                       '0' + rand() % 10];
        
        self.description = desc;
        self.dateCreated = [[NSDate alloc] init];
        self.highPriority = FALSE;
        self.finished = FALSE;
    }
    return self;
}

-(void) dealloc
{
    [taskId release];
    [description release];
    [dateCreated release];
}
@end

Now we will create list of tasks and place them in grouped table view. For view to know what to display, our controller will have to implement UITableViewDataSource protocol which requires
two methods: tableView:numberOfRowsInSection: and tableView:cellForRowAtIndexPath:
These methods tell the table view how many rows it should display and what to display in each row.
In second method we will implement standard pattern used by lists in UITableViews - reusing cells with specific identifier, in our case UITableViewCell. This prevents us from running out of memory.

UITableView is a container for UITableViewCells. A cell consists of a content view and an accessory view. Content view is where we display the content of our cell (wow?! :)) and has three subviews textLabel,detailTextLabel and imageView. Accessory is place where we keep additional buttons,checkboxes…

A UITableView asks its data source for the cells it should display
• when it's first added to the screen
• when reloadData is called
• when user scrolls
• when the table view is removed from the view hierarchy and then added back to the view hierarchy

Let's see the implementation...

#import 

@interface TasksViewController : UITableViewController
{
    NSMutableArray *tasks;
}

@end

#import "TasksViewController.h"
#import "Task.h"

@implementation TasksViewController

-(id) init
{
    self = [super initWithStyle:UITableViewStyleGrouped];
    
    if (self){
        tasks = [[NSMutableArray alloc] init];
        
        [tasks addObject:[[Task alloc] initWithDescription:@"First task"]];
        [tasks addObject:[[Task alloc] initWithDescription:@"Second task"]];
        [tasks addObject:[[Task alloc] initWithDescription:@"Third task"]];
        [tasks addObject:[[Task alloc] initWithDescription:@"Fourth task"]];
    }
    
    return self;
}

- (id)initWithStyle:(UITableViewStyle)style
{ 
    return [self init]; 
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
     // number of rows 
    return [tasks count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

    // initializing the cell with reuse option if cell already exists
    UITableViewCell *cell =
    [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                            reuseIdentifier:@"UITableViewCell"] autorelease];
    
    if(!cell){
        cell = [[[UITableViewCell alloc] 
                initWithStyle:UITableViewCellStyleDefault 
                 reuseIdentifier:@"UITableViewCell"] autorelease];
    }
        
    Task *task = [tasks objectAtIndex:[indexPath row]];    
    [[cell textLabel] setText:[task description]];
       
    return cell;
}
@end


Ok, thats great but in Wunderlist we have option to edit the position of our tasks and delete them. To make this look according to the iPhone style guide we shall also add a navigation bar where we will make this switch.
For this purpose we will use UINavigationController. We usually use it for navigation between screens, but it can also be effectively used as global toolbar.
This controller has two parts: UINavigationBar and controller displayed beneath. To start using UINavigationController we have to tell it from where to start, initialize a root controller. We shall have only one controller since we aren't switching screens.
We'll add new UINavigationController in the appDelegate application:didFinishLaunchingWithOptions:launchOption:

UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:tasksViewController]; 
 [self.window setRootViewController:navigationController]; 

Now we'll be adding navigation buttons UINavigationItems in the UINavigationBar which is displayed on the top.
All UIViewController's have UINavigationItem as their integral part. They are used when controller is displayed by the UINavigationController but we still have to specify how. UINavigationBar has three default parts leftBarButtonItem, rightBarButtonItem and titleView. Here's how we set the buttons in the init method of TasksViewController

// Setting already available edit button
    [[self navigationItem] setLeftBarButtonItem: [self editButtonItem]];  
    // Set the title
    [[self navigationItem] setTitle:@"Tasks"];

Now when we have all the buttons let's implement the functionality for them...
We have to implement the way data is deleted and animated afterwards. We'll have to implement tableView:commitEditingStyle:forRowAtIndexPath: which receives two arguments, UITableViewCellEditingStyle in this case UITableViewCellEditingStyleDelete and NSIndexPath of the deleted row.

-(void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Are we deleting row?
    if (editingStyle == UITableViewCellEditingStyleDelete){
        
        // Remove object from tasks
        [tasks removeObjectAtIndex:[indexPath row]];
        
        // Remove object from tableView
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] 
                         withRowAnimation:UITableViewRowAnimationFade];
        
    }
}

Run the app and you will see the toolbar with our edit button. Press it and … it works?! How?
Thats one of the treats in iOS programming we don't have in other environments. You noticed in the previous steps that we added editButtonItem without instantiating it, importing it or without using any other action. That's because every controller has editButtonItem property automatically linked to default setEditing:animated: which sets our table in editing mode. When we press the button for the second time ,it calls our commitEditingStyle:forRowAtIndexPath: and saves current status of the UITableView. Thats why our app works without adding any extra code. Cool...

One more thing to do is to implement tableView:moveRowAtIndexPath:toIndexPath: which will enable us to reposition our rows

-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{   
    // Get the object we are going to move
    Task *task = [tasks  objectAtIndex:[destinationIndexPath row]];
    
    // We will delete and move it to another location so well have to retain it
    [task retain];
    
    // Remove it an move it to other place
    [tasks removeObjectAtIndex:[sourceIndexPath row]];
    [tasks insertObject:task atIndex:[destinationIndexPath row]];
    
    // We no longer need it 
    [task release];
}


Now we have everything what we need for editing (moving, deleting) our table. Run the app and test our cool new additions.

Now let's insert rows. We'll do this as they do in the Wunderlist, by entering text in the text bar above the list box. Lets first add the text field in the header view of the UITableView. First we have to implement tableView:viewForHeaderInSection: and tableView:heightForHeaderInSection: for TableView to know what to display and UITextFieldDelegate to receive actions from the text field.

@interface TasksViewController : UITableViewController 
{
    UIView *headerView;
    NSMutableArray *tasks;
}

-(UIView *) headerView;
@end

- (UIView *)tableView:(UITableView *)tv viewForHeaderInSection:(NSInteger) sec
{ 
    return [self headerView]; 
}

- (CGFloat)tableView:(UITableView *)tv heightForHeaderInSection:(NSInteger)sec
{
    return [[self headerView] frame].size.height;
}

And finally implementation of the haderView with cool see through texbox...

-(UIView *)headerView
{
    if (headerView) {
        return headerView;
    }   
    
    // Container for our textField
    float w = UIScreen.mainScreen.bounds.size.width;   
    
    UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(8.0, 8.0, w - 16.0, 30.0)];
    textField.placeholder = @"Add a new task";
    textField.borderStyle = UITextBorderStyleLine;
    UIColor *color = [[UIColor alloc] initWithRed:0.0 green:0.0 blue:0.0 alpha:0.3];
    textField.backgroundColor = color;
    [color release];
    textField.borderStyle = UITextBorderStyleRoundedRect;
    textField.font =  [UIFont systemFontOfSize:14];    
    textField.textColor = [UIColor whiteColor];
    [textField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];  
    textField.delegate = self;
    
    headerView = [[UIView alloc] initWithFrame:CGRectMake(0,0, w, 48)];  
    [headerView addSubview:textField];
    [textField release];
    
    return headerView;
}

We have to enable user to use the text box automatically so when he/she presses return, data is automatically added into the table.
For this will be using textFieldShouldReturn: method which will put current text from the UITextField into table when we press enter.

- (BOOL)textFieldShouldReturn:(UITextField *)textField{
    [textField resignFirstResponder];
    
    if ([textField.text length] > 0) {
        [tasks addObject:[[Task alloc] initWithDescription:textField.text]];
        [self.tableView reloadData];
        [textField setText:@""];
    }
   
    return YES;
}

It's seems we're making progress, but it doesn't look as "rich" as it should be. We'll add special cells which will enable us to click finished tasks and mark them as high priority. With current (default) implementation we can use textLabel, detailTextLabel and imageView which aren't enough. We'll have to subclass the UITableViewCell and make custom cell for our special purposes.

Create a new subclass of UITableViewCell from the menu. I will name it TaskCell. We'll add a UILabel for description easily but iPhone doesn't have a checkbox for our priority/finished values so we'll have to create one on our own.. We'll use UIButton for this purpose and set the background image which will make them look like checkboxes. We come to the part in which I suck and that is drawing so I will have to improvise... :(

@interface TaskCell : UITableViewCell
{
    UILabel *descriptionLabel;
    UIButton *finishedCheckBox;
    UIButton *highPriorityCheckBox;
}

-(void)setTask:(Task *)task;


And for implementation we'll define two methods, first is the standard initWithStyle:reuseIdentifier where we will initialize all the cell components.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        // Standard label initalization
        descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
        descriptionLabel.backgroundColor = [UIColor clearColor];
        [self.contentView addSubview:descriptionLabel];
        [descriptionLabel release];
        
        // Define images for all three states
        finishedCheckBox = [[UIButton alloc] initWithFrame:CGRectZero];
        [finishedCheckBox setBackgroundImage:[UIImage imageNamed:@"checkbox.png"] 
                                    forState:UIControlStateNormal];
        [finishedCheckBox setBackgroundImage:[UIImage imageNamed:@"checkbox-pressed.png"] 
                                    forState:UIControlStateHighlighted];
        [finishedCheckBox setBackgroundImage:[UIImage imageNamed:@"checkbox-checked.png"] 
                                    forState:UIControlStateSelected];
        [finishedCheckBox setCenter:CGPointMake(100,200)];
        [finishedCheckBox addTarget:self action:@selector(toggleCheckBox:) 
                   forControlEvents: UIControlEventTouchUpInside];
        [self.contentView addSubview:finishedCheckBox];
        [finishedCheckBox release];
        
        highPriorityCheckBox = [[UIButton alloc] initWithFrame:CGRectZero];
        [highPriorityCheckBox setBackgroundImage:[UIImage imageNamed:@"star.png"] 
                                    forState:UIControlStateNormal];
        [highPriorityCheckBox setBackgroundImage:[UIImage imageNamed:@"star.png"] 
                                    forState:UIControlStateHighlighted];
        [highPriorityCheckBox setBackgroundImage:[UIImage imageNamed:@"star-checked.png"] 
                                    forState:UIControlStateSelected];
        [highPriorityCheckBox setCenter:CGPointMake(100,200)];
        [highPriorityCheckBox addTarget:self action:@selector(toggleCheckBox:) 
                   forControlEvents: UIControlEventTouchUpInside];
        [self.contentView addSubview:highPriorityCheckBox];
        [highPriorityCheckBox release];
        
    }
    return self;
}

Second method is layoutSubwiews. You probably noticed that we didn't' specify size of the used elements. That's because we don't know them at this point. Just before the cell is loaded message is sent to layoutSubwiews and at this time dimensions are available. We'll define three containers for each element spreading them equally on the cell.

- (void) layoutSubviews
{
    [super layoutSubviews];
    
    // Checkbox and texbox dimensions and positions
    CGRect bounds = [[self contentView] bounds]; 
    float h = bounds.size.height;
    float w = bounds.size.width;
    float c = 15.0;
    float x = (h-c)/2;
    float valueWidth = 40.0;
    float space = 10.0;
    
    CGRect contentFrame = CGRectMake(x, x, c, c);
    [finishedCheckBox setFrame:contentFrame];
    
    contentFrame.origin.x += contentFrame.size.width + space; 
    [highPriorityCheckBox setFrame:contentFrame];
    
    contentFrame.origin.x += contentFrame.size.width + space; 
    contentFrame.size.width = w - ((h-c)/2 + valueWidth + space * 4); 
    [descriptionLabel setFrame:contentFrame];
    
}


Run the app and see what happens. We started from scratch and now we have a fully functional good looking UITableView with few neat tricks.
Unfortunately I haven't got more time, but when I find some I will try to add more functions to the UITableView, add SQLite storage and connect it to apropriate API by using ASIHttp and JSON framework. Hope I helped someone with this....