Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a UITableViewCell with different subviews reusable?

I have a UITableView in which I display, naturally, UITableViewCells which are all of the same class, let's call it MyCell. So I have one MyCell.xib, one MyCell.h and one MyCell.m.

Unfortunately, this cells do contain one subview, which holds varying content, e.g. a train subview and a car subview. So if the UITableView is in need of a new cell, it's always a MyCell but sometimes it contains a train subview and sometimes a car subview.

Now, here is my problem: How to make MyCell properly reusable? The cell itself is reusable as intended (In the .xib I defined it's identifier) but it's subview has to be created again and again for every cell. My first idea was to change the identifier of MyCell depending on it's content but unfortunately, reuseIdentifier can't be changed on runtime. I could, however, implement my own - (NSString *) reuseIdentifier {} which I guess would work, though I wouldn't consider it great style. Is there a better way to do this?

Many thanks in advance!

EDIT: I realize I need to add that the subviews are stored in their own classes/xibs to keep their code seperated.

like image 530
Philipp Schlösser Avatar asked Apr 21 '11 16:04

Philipp Schlösser


3 Answers

Instead of adding subviews to cells I'd suggest that you create for every kind of cell your own class. If you have the kinds: train, car, bike, boat and airplane I would create five subclasses.

As I understand Apple the reuse mechanism with the identifier is just for that case: different types of cells get their own identifier, not every single cell a special one. Just to point how I interprete the whole thing.

In Apple's Table View Programming Guide for iOS / Characteristics of Cell Objects, the 3rd paragrpah delivers some insight into the meaning of the reuse identifier.

I've written myself a small TableViewCellFactory class which makes my life easier to create cells with the interface builder and have those in my app within minutes.

First of all a small example on how to use cellForRowAtIndexPath and the factory as well as setting content for a cell.

I create a fresh cell with the factory which needs the tableView so it can handle the reuse logic. Next thing is to let a method fill in the content for the cell. In this case it's a cell which shows a video clip with some text.

Data Source delegate method and helper

- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)anIndexPath
{
    VideoClipTableViewCell *cell = [TableViewCellFactory videoClipTableViewCellWithTableView:aTableView];

    [self configureVideoClipCellWithCell:cell andIndexPath:anIndexPath];

    // code to decide what kind of cell not shown, but it could be here, just move the model
    // access code from the configure cell up here and decide on what you get

    return cell;
}

Next comes the data source helper to put content into the cell. Get the content from my model array and set the properties. Note, this does everything by reference, nothing is returned.

- (void)configureVideoClipCellWithCell:(VideoClipTableViewCell *)aCell andIndexPath:(NSIndexPath *)anIndexPath
{
    VideoClip *videoClip = [videoClips objectAtIndex:anIndexPath.row];

    aCell.videoTitleLabel.text = videoClip.title;
    aCell.dateLabel.text = videoClip.date;

    // more data setting ...
}

TableViewFactory

This class consists mainly of convenience methods and some boilerplate methods to do the real work.

// Convenience static method to create a VideoClipTableViewCell
+ (VideoClipTableViewCell *)videoClipTableViewCellWithTableView:(UITableView *)aTableView
{
    return [self loadCellWithName:@"VideoClipTableViewCell" tableView:aTableView];
}

// method to simplify cell loading
+ (id)loadCellWithName:(NSString *)aName tableView:(UITableView *)aTableView
{
    return [self loadCellWithName:aName 
                        className:aName
                       identifier:aName
                        tableView:aTableView];
}

// method with actually tries to create the cell
+ (id)loadCellWithName:(NSString *)aName 
             className:(NSString *)aClassName 
            identifier:(NSString *)anIdentifier 
             tableView:(UITableView *)aTableView
{
    UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:anIdentifier];

    if (cell == nil) {
        UINib * nib = [UINib nibWithNibName:aName bundle:nil];  

        NSArray * nibContent = nil;

        nibContent = [nib instantiateWithOwner:nil options:nil];

        for (id item in nibContent) {
            if ([item isKindOfClass:NSClassFromString(aClassName)]) {
                cell = item;
            }
        }
    }
    return cell;
}

I've thrown out the whole error and exception handling just to keep the example short. If someone is interested I'd add the code.

Some important things about the usage is:

  • The connected class name, the reuse identifier and the nib name are all the same so a cell can be created with only one string constant, else the long loadCellWithName has to be used.

  • Don't forget to set the reuse identifier in interface builder.

  • The nib should contain only one TableViewCell (can be changed with some coding though)

  • Don't set outlets of the File's Owner, use those of the tableViewCell

  • Set the class identity of the cell to a corresponding class which should be created foremost

  • Look at the screenshot

enter image description here

Thoughts on subclassing own custom cells

It's actually easy to subclass your own cell, add a few properties to it, make them available in IB with outlets, choose the new extended class in IB for your nib file.

The main problem is interface itself. It's not easily done to have different kinds of cells based on a custom cell in interface builder. The first approach would be to copy the nib file, rename it and use it with all the existing references and link the new ones to differing outlets. But what happens if the base cell has to be changed? Going through all kinds of inheriting cells could be a tedious task.

I just stumbled across Custom views in Interface Builder using IBPlugins on Cocoa with Love. It's a nice tutorial how to extend the components Library in IB. Our custom base cell could become an item in the library and become the template we've been looking for. I think this approach is the right way to choose. Still, looking at necessary steps, it's not just done within 5 minutes.

Interface builder is a helpful tool, allowing us to rapidly create views, but when it comes to reusability through subclassing, there are big steps necessary to create maintainable views. A pity.

Creating the views with code only I think one is better off with subclassing if it comes to more than one level of inheritance or many ancestor classes for just one base view.

EDIT

On the other hand, Apple warns about excessive use of subviews in a cell:

However, if the content of a cell is composed of more than three or four subviews, scrolling performance might suffer. In this case (and especially if the cell is not editable), consider drawing directly in one subview of the cell’s content view. The gist of this guideline is that, when implementing custom table-view cells, be aware that there is a tradeoff between optimal scrolling performance and optimal editing or reordering performance.

Right now any approach has its drawbacks and advantages:

  • Too man subviews will hit performance, easily done with IB

  • Drawing with code will result in a hard to maintain code base but will perform better

  • Skipping IB makes subclasssing of template cell classes easier

  • Hierarchy through subclassing difficult to achieve with IB with nib files

like image 89
Nick Weaver Avatar answered Oct 23 '22 07:10

Nick Weaver


There are a couple of different ways to do this. You need a way to access that subview and reset or change it on reuse.

  1. You could subclass UITableViewCell with your own class of cell that has a property for the train or car view. That way you could access and change that view when the cell is reused.

  2. Assign a different identifier to each type of cell:

`

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        static NSString *CarCellIdentifier = @"CarCell";
    static NSString *TrainCellIdentifier = @"TrainCell";
        if(indexPath == carCellNeeded) { 
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CarCellIdentifier];
             if (cell == nil) {
                 cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CarCellIdentifier] autorelease]; 
             [cell addSubview:carView];
        }
        } else if(indexPath == trainCellNeeded){ 
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TrainCellIdentifier];
                if (cell == nil) {
                  cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TrainCellIdentifier] autorelease]; 
            [cell addSubview:trainView];
             }
        }
      return cell; 
}
  1. Or assign a special tag to that sub view you are adding and when the cell comes back around again to be reused you can access that specific subview by its tag.
like image 43
Dancreek Avatar answered Oct 23 '22 07:10

Dancreek


I would add both custom subviews to the nib and connect them to an outlet. And depending on the content I would hide one of them when you configure the content of your cell.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"CellIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = /* load from nib */
    }
    if (/*indexPath conditionForTrainCell*/) {
       cell.trainSubview.hidden = NO;
        cell.carSubview.hidden = YES;
        // configure train cell
    }
    else {
       cell.trainSubview.hidden = YES;
        cell.carSubview.hidden = NO;
        // configure car cell
    }
    return cell;
}
like image 28
Matthias Bauch Avatar answered Oct 23 '22 08:10

Matthias Bauch