PrEV
Thoughts from a NeXTStep Guy on Cocoa Development

UITableViewCell from a NIB file

Feb 12, 2009 by Bill Dudney

I have had several questions recently on how to use Interface Builder to create table view cells. And more specifically how to add stuff like controls to the table view cells and have them invoke methods on a controller object that can mediate the connection to the model.

The Settings application's 'Sounds' configuration section has an example of what I'm heading towards. Almost every cell in this table view has a control in it. When these controls are tweaked (volume, vibrate on or off etc) underlying stuff is changed. There are of course lots and lots of ways to do that. We present one in the book, I'll present a slightly different way here.

Here is a screen shot of my exceedingly ugly and non HIG conformant application that I build to illustrate the idea. Please don't copy it, use it as it was intended, for you to learn about table view and their cells.

The Model that underlies this application is just a string holder, very simple. The model has one field called 'value' that is displayed in the text of the table view cell. The model also has two methods on it called ping and play. These methods are representative of what you might have in a real model, i.e. they don't really do anything. Imagine that they do something important though to your model. Your UI needs to allow the user to invoke this functionality from the table view cell. Making that happen will be the topic of the remainder of this post.

If you'd like to follow along you can get the code here or you can build it yourself starting from the 'Navigation Based Application' template.

Let's start with the model, remember that the model is the underlying logic of your application captured in classes. This is a really simple model, in your application I'm sure its more complex, but simple or complex you should have a model that captures the essence of your application. Here is the header for the Model class.

@interface Model : NSObject { NSString *value; }
+ (id)modelWithValue:(NSString *)newValue; - (id)initWithValue:(NSString *)newValue;
@property(nonatomic, retain) NSString *value;
- (void)play; - (void)ping;
@end

Not a lot here, and the implementation file is equally simple;

@implementation Model
@synthesize value;
+ (id)modelWithValue:(NSString *)newValue { return [[[self alloc] initWithValue:newValue] autorelease]; }
- (id)initWithValue:(NSString *)newValue { self = [super init]; if(nil != self) { self.value = newValue; } return self; }
- (void)play { NSLog(@"playing %@", self.value); }
- (void)ping { NSLog(@"pinging %@", self.value); }
@end

Now that we have a model set up lets look at displaying that model in our table view. The template that I used to create this sample project already gave me a UITableViewController so all I need to do is get the data into it.

@interface RootViewController : UITableViewController {   NSArray *models; }
@property(nonatomic, retain) NSArray *models;
@end

In the viewDidLoad I initialize this list of model objects. For the example I just hard coded everything but using SQLite is a much better solution. The tableView:numberOfRowsInSection: method just returns the count of this array since I only have one section in the example. Finally the tableView:cellForRowAtIndexPath: method does the real work of creating the new TableViewCell from the nib file. Here is the code.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {   // in IB I set the identifier to Cell on the TVC in CustomTVC.xib   static NSString *CellIdentifier = @"Cell";      CustomTVCell *cell = (CustomTVCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];   MiniController *mini = nil;   Model *model = [self.models objectAtIndex:indexPath.row];   if (cell == nil) {     mini = [[MiniController alloc] init];     [[NSBundle mainBundle] loadNibNamed:@"CustomTVC" owner:mini options:nil];     cell = mini.cell;   } else {     mini = cell.controller;   }      // setting the model on the mini controller updates the elements in the   // cell, in this example its just the label   mini.model = model;
  return cell; }

There are two custom classes here, the CustomTVCell and the MiniController. The basic idea is that each table view cell in this table view is its on little MVC triad. While it might seem like overkill its a great way to factor things if you need extra functionality on each cell. Lets look at the custom cell first. Here is the whole interface and implementation.

@class MiniController;
@interface CustomTVCell : UITableViewCell {   IBOutlet MiniController *controller; }
@property(readonly) MiniController *controller;
@end
@implementation CustomTVCell
@synthesize controller;
@end

This is really just a way to add a reference to the controller. That reference is used so we know which controller to set the model on in the cellForRowAtIndex:. Since the table view has a great bit of functionality to reuse cell's we can reuse the controllers as well as long as we have this connection. Otherwise we'd have to somehow duplicate the reuse bits in the table view for our controllers, blah.

Next we get to the MiniController. Here is the interface.

@class Model; @class CustomTVCell;
@interface MiniController : NSObject {   CustomTVCell *cell;   UILabel *label;   Model *model; }
@property(nonatomic, retain) IBOutlet CustomTVCell *cell; @property(nonatomic, retain) IBOutlet UILabel *label; @property(nonatomic, retain) Model *model;
- (IBAction)play:(id)sender; - (IBAction)ping:(id)sender;
@end

The cool thing about doing your reuse-able table view cells this way is that I have actions that I can connect controls in the cell to that will not be long and nasty. I don't have to look at the sender and its superclass to find out which row the control was on when it was clicked. This controller gets clicks from the buttons connected to it.

Apple-tab-span

Finally the implementation of the MiniController. Lets start with the actions first.


- (IBAction)play:(id)sender {   [self.model play]; }
- (IBAction)ping:(id)sender {   [self.model ping]; }

Just forwarding on the action request to the model here. In a more sophisticated example you might want to have the view controller that manages the mini-controller as well and then invokes some more sophisticated logic.

Next I want to show you a little trick that I use to avoid having to implement the set methods that I would normally get from an @synthesize statement. If you have never messed with Key-Value-Observing its some really powerful stuff. Now if you look at this and want to reach through the computer and smack me, simple pretend this last bit is not here and replace the setModel: method to set the label's text.

In the init method I register the mini-controller object to observe changes to the model property. Then when its changed I get a call back letting me know. Only additional piece is to de-register in the dealloc method. Here is the code.

- (id)init {   self=[super init];   if(nil != self) {     // observing makes it so we don't have to implement the set method     [self addObserver:self forKeyPath:@"model"                options:NSKeyValueObservingOptionNew context:NULL];   }   return self; }
- (void)observeValueForKeyPath:(NSString *)keyPath                        ofObject:(id)object                          change:(NSDictionary *)change                        context:(void *)context {   if([keyPath isEqualToString:@"model"]) {     label.text = model.value;   } }
- (void)dealloc {   [self removeObserver:self forKeyPath:@"model"];   self.cell = nil;   self.model = nil;   self.label = nil;   [super dealloc]; }

Again this is a trick that saves me the hassle of rewriting my set method when I change an attribute of the property.

This is only one approach to designing your table view cells in Interface Builder. Your comments are of course as always very welcome.



Comments:

Thanks for the post - loading a UITableViewCell from a nib is a not very obvious process.

One quick note: Generally NSString properties are set to copy. This is to safeguard against being passed an NSMutableString instance and having its contents mutate without your knowledge. If an immutable instance is passed, the copy message will result in a retain, so there isn't a performance concern.

Cheers!

Posted by Joshua on February 12, 2009 at 10:00 AM MST #

Anyone find a testing framework for doing functional testing through the iPhone Simulator UI? Something like Selenium, but targeted to scripting user UI interaction with the iPhone and examining results.

Unit testing can be accomplished outside of the UI, but having that extra layer of assurance of correct app behavior would be a plus.

Posted by Neal on March 30, 2009 at 09:54 AM MDT #

Thank-you so much!!!!! I've been struggling with this for a while.

Posted by Ethan on May 01, 2009 at 10:12 AM MDT #

Bill,

Using this approach, any pointers on how to set and change a background image for a custom TableViewCell from a NIB? I have a background image showing in each cell (via a UIImageView currently) , but when a user selects a row, I want the cell to be highlighted by swapping in a brighter version of the image. I've looked through a bunch of docs, but they are all geared towards using a custom TableViewCell class and UIView methods.

Thanks, Mike

Posted by Mike Murray on May 21, 2009 at 05:43 AM MDT #

It seems like this would create a retain cycle which is going to leak memory, since your controller retains your cell and the cell retains your controller, so neither of them will ever get dealloced.

Or am I missing something?

Posted by Dave on August 17, 2009 at 10:11 AM MDT #

I'm using something similar, and it's working, but does the reuse/reuseidentifier process still work when loaded from nib?

In your example, you kept the call to get a reusable cell if available:
CustomTVCell *cell = (CustomTVCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

But, when not available (cell==nil), and you create one from a nib, you don't define the reuseidentifer property so it can be reused later. Is it possible to do? My loaded cell subclassed from UITableViewCell doesn't seem to inherit or support the reuseidentifier. [cell reuseIdentifier:CellIdentifier] crashes with 'unrecognized selector';

Am I missing something?

Posted by Bill on September 25, 2009 at 07:40 AM MDT #

the reuse id is set in the nib file

Posted by Bill Dudney on September 25, 2009 at 07:40 AM MDT #

Another solution to load the view form a Nib file is to create a UIView, not a UITableViewCell, and to associate that to a property of the file owner:

@interface MyTableViewCell : UITableViewCell {
UIView *contents;

UIImageView *imageView;
UILabel *textLabel;
UILabel *detailTextLabel;
}

@property (nonatomic, retain) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) IBOutlet UILabel *textLabel;
@property (nonatomic, retain) IBOutlet UILabel *detailTextLabel;

@end

@implementation MyTableViewCell

@synthesize imageView, detailTextLabel, textLabel, contents;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]) {
[[NSBundle mainBundle] loadNibNamed:@"MyTableViewCell" owner:self options:nil];
[self.contentView addSubview:contents];
}
return self;
}

@end

In this way your cellForRowAtIndexPath will be:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *CellIdentifier = @"MyTableViewCell";

MyTableViewCell *cell = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[MyTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}

//configure the cell
return cell;
}

Posted by Oracle on October 23, 2009 at 08:23 AM MDT #

Mike: There is a leak, but not a retain cycle. The controller retains the cell, but the cell does not retain the controller. The controller, however, is allocated in cellForRowAtIndexPath, but never released.

You can fix the leak by making the cell own the controller. That's a bit unusual, but it lets you still piggyback off of UITableView's cell cache, instead of having to write your own cache.

Posted by Randy Becker on December 12, 2010 at 09:29 AM MST #

Post a Comment:
  • HTML Syntax: Allowed