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.
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
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
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.
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.
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;
}
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With