5

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.

Philipp Schlösser
  • 5,179
  • 2
  • 38
  • 52
  • Can you describe the content a bit more? Are you referring to many different subviews? – Nick Weaver Apr 21 '11 at 16:26
  • As I said, I have a (finite) number of different subview types which I want to reuse. So if the app needs a "Train Cell" I fill my "MyCell" with a "Train subview". Currently, I create the subview again and again but I'd rather like to keep it in memory and just refill it, just like with TableViewCells. That means, if I would have 5 "MyCells" cached (because not more than 5 are displayed at once) and I have 2 different subview types, the app should cache 5*2 MyCells, 5 of them filled with a train subview, 5 filled with a car subview. – Philipp Schlösser Apr 21 '11 at 16:39

4 Answers4

11

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

Nick Weaver
  • 47,228
  • 12
  • 98
  • 108
  • Thanks Nick, this is quite an impressive answer. Still, I see one issue: The Cell I called "MyCell" contains quite a few views (besides the changing one) and also quite a lot of code. I would have to copy/paste the views into every different cell (boat, train...) since there is no way of inheriting the elements of my "MyCell" or am I mistaking? – Philipp Schlösser Apr 21 '11 at 23:09
  • @Phlibbo I did some testing and reading on it so I've added what I have found out. – Nick Weaver Apr 22 '11 at 00:15
  • Simple and makes sense, I was toying with the idea of moving subviews around based on whether or not content was available. This is much easier, less buggy, and should be more efficient since it is truly re-using the cells. Thanks for this "duh" moment Nick, great answer! – Chris Wagner Sep 04 '13 at 21:00
  • thanks for the very detailed explanation and sourcing the Apple docs – emilebaizel Nov 19 '13 at 19:28
3

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.
Dancreek
  • 9,524
  • 1
  • 31
  • 34
  • Thanks for your response. One question: Would your way of assigning different identifiers still work if I have my own Subclass ob UiTableViewCell with it's own xib? I'm not sure about that because I defined the Identifier in the xib file. – Philipp Schlösser Apr 21 '11 at 23:16
  • You would just use an initWithNib instead of initWithStyle. You would still have the dequeueReusableCellWithIdentifier line. Just make sure that the identifier you specify in the nib and in the code are the same. – Dancreek Apr 23 '11 at 16:22
1

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;
}
Matthias Bauch
  • 89,811
  • 20
  • 225
  • 247
  • Thank you, this in fact would be a solution. Unfortunately, some subviews are backed by quite a lot of code which I would prefer to keep separated. Could I somehow do that while using your solution? – Philipp Schlösser Apr 21 '11 at 16:42
  • Depends how you do the separation right now. But I don't see a problem. In theory you could just replace the adding and removal of the subview with the unhiding and hiding of it. – Matthias Bauch Apr 21 '11 at 16:51
  • I know it is a stupid question, but if I have a .xib with a customized UITableViewCell, how do I insert a custom view for which I have a .xib and code files? I thought it's possible but just can't find it. Thanks! – Philipp Schlösser Apr 21 '11 at 23:20
  • Add an UIView object in the cell nib and change it's class (In the identity tab of the inspector) to your custom class. Then you'll need a IBOutlet in the cell subclass so you can connect them. – Matthias Bauch Apr 22 '11 at 01:18
0

the simplest is to make a custom UITableViewCell subclass and create a xib for it. Set the root view in the xib as an uitableviewCell and set its class to your UITableViewCell subclass. Set the file owner class as your TableViewController subclass and add all the subviews you want to it. Then you can simply:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString * CellIdentifier = @"cellIdentifier";

    TableViewMessageCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil)
    {
        cell = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([TableViewMessageCell class])
                                              owner:self
                                            options:nil] lastObject];
    }

    Message * message = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.message      = message;

    return cell;
}
Nicolas Manzini
  • 8,379
  • 6
  • 63
  • 81