Table views are not only about displaying arbitary data in form of a list. Today, I will show you some little tweaks to create some nice user interfaces with the help of table views and their cells. I’ll present some tactics I’ve learned from the CocoaCast podcast made by Boris Klaydman. If you don’t know it yet, I recommend you to take a look. Those guys do an amazingly good job ;)
Some theory about table views
In order to use table views in your iPhone app, you take adventage of the UITableView class in combination with a subclass of UITableViewController. The controller helps you to bring the table view to life by acting as it’s data source. The table view will ask it’s data source, “What should I display at row xyz?”, and the data source will give him the answer. A UITableView consists of cells, instances of UITableViewCell or it’s subclasses. You’ll use the subclasses if you want to present cells with custom behaviour to the user.
Okay, enough theory – let’s fire up Xcode for some table view fun ;)
Here’s a picture of the complete user interface that we are about to build:
Pretty neat, huh?
Start a new project
In Xcode click File > New Project, choose the Navigation-based Application as template for our project and name it “TableViewFun”. This will create a working application with a table view that lives inside a navigation controller. We won’t develop it from scratch this time since we want to concentrate about the customization of our table view cells. But if this is the first time for you working with UITableView I’ll briefly scan through the code and explain what has been done for us.
The most important part is the UITableViewController subclass that is named “RootViewController” in this template. When you open RootViewController.m, you’ll see a whole bunch of code that has been generated for us. The interesting part of that is the section marked as #pragma mark Table view methods. The following four methods are the data source methods that are used to set up our table and fill it with content:
numberOfSectionsInTableView:
Here we’ll tell the table view how many sections we want to display in our table.
tableView:numberOfRowsInSection:
This one will tell the table view how many rows live in each section. This can be some static value(s) or, considering you’ll want to show the contents of an NSArray in the table view, it could be the return value of a method call, e.g. [myArray count]
tableView:cellForRowAtIndexPath:
The actual table view cell for a given index path. An NSIndexPath is a C-struct that helps you navigate through a table view by indicating where you are, e.g. section 1, row 5.
Because memory is limited on a mobile device like the iPhone, we’ll try to reuse cells in this method instead of always creating new ones. We will come to that in a minute ;)
tableView:didSelectRowAtIndexPath:
This will trigger some custom action for selecting a table view row. In most cases this would be pushing a new view controller onto the navigation stack for drilling down a table view, but we will use it for some other purpose.
There are some more (optional) data source methods defined in the UITableViewDataSourceProtocol but we won’t need them at this time. Feel free to browse the Apple docs if you want to know more about the remaining methods.
Some interface work
Open RootViewController.xib for some minor changes concerning the table view. It should open a view with an embedded table view. Click the table view to select it. In the Attributes section of the Inspector (+1) choose the “Grouped” style instead of the plain one. Save and return to Xcode.
Since this is all about customized cells, we’ll have to create them too. You could do this programatically, but I’ll present the way you would do it with the help of Interface Builder (IB).
Control-click the Classes folder in “Groups & Files”, choose “New File” from the context menu and select “UITableViewCell subclass” from the “Cocoa Touch Class” section. Name that class TextFieldCell (this will be the cell holding a UITextField) and create the .h and .m files.
Now we’ll have to create the correspondig NIB (XIB) file. Right-click on the “Resources” folder, choose “New File”, create an empty XIB from the “User Interfaces” section of the iPhone OS group and name it “TextFieldCell.xib”. Open the file by double-clicking which will take you back to IB. Open the Library and drag a UITableViewCell onto the little window with both the “File’s Owner” and “First Responder” in it. Double click it and up pops another window which represents the table view cell. We’ll customize it by dragging a UITextLabel and a UITextField on it. Play around with their attributes until you have something like this:
Set File’s Owner’s class to “RootViewController” and the UITableViewCell’s to “TextFieldCell”. In the Identity section (+4) for the table view cell create an outlet named textField for the UITextField instance you dropped onto our custom cell. Connect the outlet with the actual text field and define the outlet in TextFieldCell.h (e.g. by dragging it from IB to Xcode). Create and synthesize a corresponding property as well and don’t forget to release textField in the dealloc method. After that, create an outlet for our TextFieldCell named “textFieldCell” in RootViewController, connect it, create poperties, synthesize them and edit the RootViewController’s dealloc method.
TextfieldCell.h
#import
@interface TextFieldCell : UITableViewCell {
IBOutlet UITextField *textField;
}
@property (nonatomic, retain) IBOutlet UITextField *textField;
@end
RootViewController.h
#import "TextFieldCell.h"
#import "SwitchCell.h"
@interface RootViewController : UITableViewController {
IBOutlet TextFieldCell *textFieldCell;
}
@property (nonatomic, retain) TextFieldCell *textFieldCell;
@end
After that we’ll create another custom cell, the one with an UISwitch in it. It’s pretty straight forward if you completed our first custom cell. So I assume you can do it on you own ;) Please name the UITableViewCell subclass “SwitchCell” and the corresponding XIB file “SwitchCell.xib”. The result should look like this
SwitchCell.h
#import
@interface SwitchCell : UITableViewCell {
IBOutlet UISwitch *turnOnSwitch;
}
@property (nonatomic, retain) IBOutlet UISwitch *turnOnSwitch;
@end
RootViewController.h
#import "TextFieldCell.h"
#import "SwitchCell.h"
@interface RootViewController : UITableViewController {
IBOutlet TextFieldCell *textFieldCell;
IBOutlet SwitchCell *switchCell;
}
@property (nonatomic, retain) TextFieldCell *textFieldCell;
@property (nonatomic, retain) SwitchCell *switchCell;
@end
Adding the glue
Now that we have our customized cells it’s time to glue them to our table view. But first we have to do some more basic stuff. That is, to tell our table view the number of sections and rows for each section. For this example, we’ll have three sections (0, 1, 2). The first two sections (0, 1) will have one row each and the third section (2) will have three rows. The following snippet shows you how to do it:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
switch (section) {
case 0:
return 1;
case 1:
return 1;
case 2:
return 3;
default:
// We'll never reach that case, but just to
// make sure that something will be returned
return 0;
}
}
Build an run our application and see what happens:
Hmm, just what we wanted but it looks kinda naked, doesn’t it? What about adding some headers by implementing the tableView:titleForHeaderInSection: method:
- (NSInteger)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
switch (section) {
case 0:
return @"Your name";
case 1:
return @"Turn me on";
case 2:
return @"Choose your destiny";
default:
// We'll never reach that case, but just to
// make sure that something will be returned
return nil;
}
}
Build and run… okay, that looks a lot better ;)
But it’s quite obvious, that no of our custom cells were used. Instead, we’re presented the standard table view cells. In order to change that, we have to edit the tableView:cellForRowAtIndexPath: method. The default implementation is the following:
- (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] autorelease];
}
// Configure the cell.
return cell;
}
As I mentioned before, it’s a good practice to reuse table cells instead of always creating new ones, cause this is always very expensive and it will create no good user experience. The default implementation uses a static identifier called “Cell” (or some other arbitary string) to reuse the table view cells. If no reusable cell could be dequeued, we have to create a new one. Here, an instance of the standard UITableViewCell class is used to create a new cell.
We have to decide which kind of cell has to be created for each section. In the first section of our table view we’ll need the TextFieldCell, in the second section we’ll need the SwitchCell and in the third and last section we will use the standard cells. We will load our two custom cells from their XIB files. In order to do that, we’ll call the loadNibNamed:owner:options: method from the mainBundle object:
[[NSBundle mainBundle] loadNibNamed:@"TextFieldCell" owner:self options:nil];
When doing like this our member variables textFieldCell and switchCell of the RootViewController class should be set to the right instances.
Take the snipped below for our implementation of tableView:cellForRowAtIndexPath: That’ll do exactly what we want.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell;
switch (indexPath.section) {
case 0:
cell = (TextFieldCell *)[tableView dequeueReusableCellWithIdentifier:@"TextFieldCell"];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"TextFieldCell" owner:self options:nil];
cell = textFieldCell;
}
break;
case 1:
cell = (SwitchCell *)[tableView dequeueReusableCellWithIdentifier:@"SwitchCell"];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"SwitchCell" owner:self options:nil];
cell = switchCell;
}
break;
default:
cell = [tableView dequeueReusableCellWithIdentifier:@"OtherCell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"OtherCell"] autorelease];
cell.accessoryType = UITableViewCellAccessoryNone;
}
break;
}
return cell;
}
Build and run and hopefully you’ll see the following:
If your app crashes, check if you connected all your outlets properly. In most cases it has something to do with that ;)
So far, so good. Let’s take care of the remaining section. Here we want to implement a multiple selection view, kinda like the HTML checkboxes. First of all, create some selection items in the tableView:cellForRowAtIndexPath: method:
//...
default:
cell = [tableView dequeueReusableCellWithIdentifier:@"OtherCell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"OtherCell"] autorelease];
cell.accessoryType = UITableViewCellAccessoryNone;
switch (indexPath.row) {
case 0:
cell.text = @"Foo";
break;
case 1:
cell.text = @"Bar";
break;
case 2:
cell.text = @"Baz";
break;
default:
break;
}
}
break;
//...
Now, when the user taps one of these three cells, we want to represent a checkmark on the selected cell. That has to be done in the tableView:didSelectRowAtIndexPath: method:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Retrieve the cell ...
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// For now we just take care of events
// for the third section of the table view
if (indexPath.section == 2) {
if (cell.accessoryType == UITableViewCellAccessoryNone) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Build, run and try “tap” one or more cells in the table view’s third section. You’ll notice a checkmark appearing at the right side, followed by a nice animation that indication that your selection has been completed.
We’re done!
Uhm… actually we’re not really done yet ;) What about our text field? We had to implement the text fields delegate methods to complete it. Otherwise we could never dismiss the keyboard that appear when we tap onto the text field. But I’ll leave that as a little practice for you ;)
I hope you enjoyed my little tutorial about table view cells. I’m not quite sure if my way is to be considered as the best possible practice, but it works fine for me ;)