Strongly typed Table View Cells to make your life easier

Lots of people seem to be having problems with designing and implementing easy to use and easy to maintain table view cells, for use in a UITableView on iOS. There is actually quite a nice way to make your life easy. In this article I’ll show the basic technique on how to do it.


We start with a new demo project, where we use the single view application template.
In this tutorial we will use the iPhone version, but the technique applies just a easily to iPad applications of course.

Let’s call the project StronglyTypedTableViewCell. In this project template we get a ViewController with a Xib file for free. Open the xib and add a table view to the view by dragging it in from the object library.

Link the UITableView to the file owner, by control-click-dragging from the table to the file owner twice. Once for the delegate, once for the data source.

We switch over to the code, and indicate in the ViewController, that is the file owner of the objects created visually in the xib file, that it conforms to the protocols required by the UITableView links: UITableViewDelegate and UITableViewDataSource. In the ViewController.h header file: change this line of code:

@interface ViewController : UIViewController
    <UITableViewDelegate, UITableViewDataSource>

We need to implement two required methods in the ViewController. We start with dummy implementation to get rid of the compiler warnings. So in ViewController.m write:

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section
{
  return 1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  return nil;
}

The first method is called first by the UITableView to find out the total number of rows that needs to be displayed in a certain section. For this demo, we are not using sections, so we just return 1 for now. We don’t have any data yet.

The second method will then be called by the UITableView for getting the cell that needs to be displayed for the rows that are visible in the View. Normally this is only the first 12 rows, when you don’t change the row height.

Great. The skeleton for the tutorial is ready. Now we need to fabricate some data. Lets work with Cars for the tutorial. Let’s structure the code in a good object oriented way and add a new class to the project called Car. (Cmd-N, or File->New File) Select Objective-C class and give it the name Car.

For this demo, a car has two properties: The Make and the Type, both of type string. So in Car.h write:

#import <Foundation/Foundation.h>

@interface Car : NSObject

 @property (nonatomic, strong) NSString *make;  
 @property (nonatomic, strong) NSString *type; 

@end

and then in Car.m we complete the implementation section by synthesizing the implementation for those properties.

#import "Car.h"

@implementation Car

@synthesize make = _make;  
@synthesize type = _type;

@end

In the ViewController, we create a property to hold a number of cars. This property doesn’t need to be public, so we can write it in the private interface section in the ViewController.m file. This is called an “Extension”. Kind of like an anonymous Category in Objective-C.

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *cars;

@end

and again we synthesize it in the implementation section (ViewController.m):

@synthesize cars = _cars;

Now we can start by creating a couple of test objects to show in the cells of the UITableView. However, to make our code a little bit better, we will implement our own init method in the Car class. We start by defining the method in the public header Car.h:

- (id) initWithMake:(NSString*) make
               type:(NSString*) type;

The implementation is very straightforward, in Car.m write:

- (id) initWithMake:(NSString*) make
               type:(NSString*) type
{
    self = [super init];
    if (self)
    {
        self.make = make;
        self.type = type;
    }
    return self;
}

Among other things, this is a reason why we prefixed the name of the instance variable (ivar) that belongs with the property with an underscore. This way we have no conflict with the parameter names in this method.

Very well, now for the data. First import Car.h in ViewController.m, to make the class available. Next, in the viewDidLoad method of the ViewController, write (or copy paste ;-)):

    self.cars = [[NSMutableArray alloc]initWithCapacity:14];

    [self.cars addObject:[[Car alloc]initWithMake:@"Volkswagen" type:@"Polo"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Ford" type:@"Mondeo"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Mini" type:@"Cooper"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"BMW" type:@"X3"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Audi" type:@"Q5"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Renault" type:@"Wind"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Chevrolet" type:@"Impala"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Porsche" type:@"928"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Lamborghini" type:@"Murchielago"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Fisker" type:@"Karma"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Nissan" type:@"Leaf"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Toyota" type:@"Auris"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Citroen" type:@"DS5"]];
    [self.cars addObject:[[Car alloc]initWithMake:@"Peugeot" type:@"205"]];

The preparations are complete. We can now show that data in the table. We change the implementation of the two methods to reflect the data we have. In the first method, we return the number of cars we have:

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section
{
    return self.cars.count;
}

In the second, we return a standard UITableViewCell, to show the information about the car, for now we will just use one of the built-in Styles. We implement the reuse mechanism that UITableView supports. This way only 12 or so table view cells will ever need to be created, and we just reuse the existing ones, that fall off screen when the user is scrolling.

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // static string is only initialized once for this method.
    static NSString *reuseId = @"carCell";

    // try to reuse an existing table view cell.
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseId]; 

    if(nil == cell)
    {
        // if no cell is available for reuse, we create it, with the same reuseId.
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseId];
    }

    // next we customize the content of the TableViewCell

    // get the object we want to show:
    Car *car = [self.cars objectAtIndex:indexPath.row];

    cell.textLabel.text = car.make;
    cell.detailTextLabel.text = car.type;

    // return the customized cell
    return cell;
}

So far so good. This is the standard way of showing data in a UITableView. Nothing new. You can run the code on your device, or in the simulator.

Now we are going to create a custom cell, with a nice, typed API. Let’s get started.

We start by creating a new class called CarCell. This class will be responsible for encapsulating the Cell itself, and providing an easy to use API for our other classes. It inherits from the built-in UITableViewCell. Cmd-N or File-> New File. Select Objective-C Class and provide the name and super class. 

 

 

We will also create a Xib, because we want to visually configure the custom cell. Cmd-N or File -> New File. This time select User Interface, Empty Xib. Name it CarCell too.


In the xib file, drag a UITableViewCell from the object library into the design surface.Next, add two labels to the cell. Increase the height of the cell a bit, and make the first label bold text.

This is important: we are going to change the identity of the UITableViewCell. Make sure the Cell itself is selected and in the Identity Inspector, change the class to CarCell. Great. This way, when the nib is loaded, an instance of our CarCell will be created.

In the properties Inspector, also setup the reuse identifier for the cell. This should match the reuse identifier we use from code.

Now we can easily create two outlets, again visually. We need to open up the Assistant Editor, and make sure that the content of CarCell.h is shown there.

Next, Control-drag from the first label to the code in the assistant editor. It will prompt to create an outlet. Let it do it’s job and give it the name makeLabel. Do the same for the second label and call it typeLabel.

By doing this visually, all the necessary code has been generated for you by XCode.

Now we only need to be able to use the custom strongly typed cell in our table. That’s the tricky bit. Here, we need to change the file owner of the CarCell.Xib. Currently it doesn’t really have one. Select the file owner icon and open the identity inspector again, and change the class from NSObject to ViewController.

We will create an IBOutlet in the ViewController to easily get access to the CarCell object when it is loaded. So again, open up the Assistant Editor. Make sure that the content of ViewController.h is showing up and control drag from the CarCell itself into the code to create the IBOutlet. Call it carCellTemplate. Make sure it is a strong property.

Great. Now we can change the implementation of the second TableViewDataSource method to use our custom cell.
Start by importing CarCell.h into ViewController.h.

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // static string is only initialized once for this method.
    static NSString *reuseId = @"carCell";

    // try to reuse an existing table view cell.
    CarCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseId]; 

    if(nil == cell)
    {
        // if no cell is available for reuse, we create it, with the same reuseId.
        // instead of using a standard UITableViewCell style, we create our custom CarCell

        // load the objects in the nib, and setup the "file owner" to self
        [[NSBundle mainBundle] loadNibNamed:@"CarCell" owner:self options:nil];

        // at this point, self.carCellTemplate points to the CarCell that
        // was defined in the Xib.

        // so now we have a reference to it.
        cell = self.carCellTemplate;
    }

    // next we customize the content of the TableViewCell

    // get the object we want to show:
    Car *car = [self.cars objectAtIndex:indexPath.row];

    // we now have a typed api to customize our cell:
    cell.makeLabel.text = car.make;
    cell.typeLabel.text = car.type;

    // return the customized cell
    return cell;
}

We are almost done. If you run the project now, you will notice that cells are not shown correctly. This is because the height of the rows has increased.

That is usually the case when you design a custom cell. Therefore we need to get the height of the cell. You can read it from the designer. eg. 90px. And we need to implement one method in the ViewController. The tableview will ask it how high each cell needs to be. Notice that this method has to be very fast, because the TableView needs to know before doing anything how high the entire content will be to setup the scrollView.

- (CGFloat)tableView:(UITableView *)tableView
    heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 90;
}

We’re done. The result is not spectacular, but now you can easily start adding other user interface elements to the cell, without having to calculate or guess their positions.

In summary, what we did: Create a custom class CarCell that represents and encapsulates the cell. It is in that class that we define the API for manipulating and customizing the contents of our custom cell. Create the layout for that custom cell in a Xib, and make sure you manage the identity of the cell, so that it creates an instance of your custom class when loading the nib. Also make sure you have an easy way to access the objects created when loading the xib. We do this by changing the file owner of that new xib, and using an IBOutlet to get access to the objects we are interested in. Pay attention to setup the Reuse Identifier, otherwise it is not as memory efficient as it could be.

There is room for improvement. Instead of assigning to the properties of our cell when customizing it, we could have just assigned the Car itself (to a new property), and make our custom cell class responsible for showing that car in the setter of that property. This way we need less knowledge about the cell itself outside that class. Better applying the Single Responsibility Principle, and gaining in Cohesion of the objects.

There is a common pitfall here. You should no longer use any of the properties of the standard UITableView cell, as they will usually overlap with or hide the UI elements you added. Also, because these cells are reused by the UITableView, make sure to set all values that are visible to the user. If you forget some, you will see strange results, due to the caching and reuse of cells. You could provide a “prepare for reuse” method to facilitate this.

Fork the code on github or download a zip.

I hope this was useful. Any feedback is very much appreciated in the comments.

kthxbye!

Leave a Reply

%d bloggers like this: