← Back

Three20 Custom Cells iPhone Tutorial

Jun 24, 2009 - Posted in Three20, Tutorials, iPhone

*Note* This tutorial is now deprecated, please refer to my new TableItem tutorial for future learnin’s

Introduction to custom three20 cells

In my last tutorial I showed you how to use CSS-like stylesheets in your iPhone Apps. Although I did make a custom cell I didn’t really show how this is done, and I’ve decided to go into a bit more detail.

In this tutorial I am going to show you how to make custom table cells in your iPhone Apps using Joe Hewitt’s Three20 library.

Custom cells are very useful, they let you do things like this:

Custom Cells

But it’s worth mentioning that i’m not going show you how to make a complete iPhone App, nor install the Three20 library in your Apps (there is already a guide on how to do this on the three20 github page).

I am going to assume that you either cloned my three20 tutorial repository from github and are following along in my Three20 stylesheets tutorial, or that you already have an app using three20 with a working tableview and datasource similar to mine (there are several ways to do this, but I am going to assume you’re using my way).

By the way, all my Three20 tutorials are available on gitHub.

So lets get started:

First things first, There are cells, and there are fields.

  • Cells control the labels, images, interface elements, etc that are actually displayed when a table cell is rendered.
  • Fields control the actual data that is fed into each label, image, interface element, etc when the table cell is rendered.

But although they usually go together, you don’t always HAVE to make a new field type for every new cell type. As long as all the data that the new cell needs is provided by an existing field, you need not make another.

For cells like the one below with a title and subtext, the data is fed by a field which holds 2 strings: one for the title and one for the subtext.

Ordinary Cells

The code that styles the above cell (BNSubtextTableFieldCell) looks like this:

Code used to style basic cells with title and subtext

And the field that feeds that cell looks like this (in the Three20 source code):

aaa

JUST changing a cell’s appearance.

To change the colors, font sizes, etc in a particular cell, all you need to do is subclass that cell (in this case BNSubtextTableFieldCell), and change it’s label’s properties.

  1. To subclass BNSubtextTableFieldCell, make BNCell.h looks like this:

    #import "Three20/Three20.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////
    //////   BNSubtextTableFieldCell     ////
    /////////////////////////////////////////
    
    @interface BNSubtextTableFieldCell : TTSubtextTableFieldCell {
    
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleTableFieldCell     ////
    /////////////////////////////////////////////////////
    
    @interface BNSubtextWithRedTitleTableFieldCell : BNSubtextTableFieldCell {
    
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
  2. Then make BNCell.m look like this:

    #import "BNCell.h"
    #import "BNDefaultStylesheet.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////
    //////   BNSubtextTableFieldCell     ////
    /////////////////////////////////////////
    
    @implementation BNSubtextTableFieldCell
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)setObject:(id)object {
    	if (_field != object) {
    		[super setObject:object];
    
    		TTSubtextTableField* field = object;
    
    		_label.text = field.text;
    		_label.font = TTSTYLEVAR(myFirstFont);
    		_label.textColor = TTSTYLEVAR(myFirstColor);
    		_label.adjustsFontSizeToFitWidth = YES;
    
    		_subtextLabel.text = field.subtext;
    		_subtextLabel.font = TTSTYLEVAR(mySecondFont);
    		_subtextLabel.textColor = TTSTYLEVAR(mySecondColor);
    	}
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleTableFieldCell     ////
    /////////////////////////////////////////////////////
    
    @implementation BNSubtextWithRedTitleTableFieldCell
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)setObject:(id)object {
    	if (_field != object) {
    		[super setObject:object];
    
    		_label.textColor = [UIColor redColor];
    	}
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    

    All we did there was change the textColor property of the cell’s _label to an ugly ass red color.

  3. Next we change the cellForClassObject method in RootViewDataSource.m to point to your new BNSubtextWithRedTitleTableFieldCell cell type.

    Make the RootViewDataSource.m look like this:

    #import "RootViewDataSource.h"
    #import "BNCell.h"
    
    @implementation RootViewDataSource
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // public
    
    + (RootViewDataSource*)rootViewDataSource {
    	RootViewDataSource* dataSource =  [[[RootViewDataSource alloc] initWithItems:
    									 [NSMutableArray arrayWithObjects:
    									 [[[TTSubtextTableField alloc] initWithText:@"Video" subtext:@"Now you can shoot video, edit it, and share it — all on your iPhone 3G S."] autorelease],
    									 [[[TTSubtextTableField alloc] initWithText:@"3-Megapixel Camera" subtext:@"The new 3-megapixel camera takes great still photos, too, thanks to built-in autofocus."] autorelease],
    									 [[[TTSubtextTableField alloc] initWithText:@"Search" subtext:@"Find what you’re looking for across your iPhone, all from one convenient place."] autorelease],
    									 [[[TTSubtextTableField alloc] initWithText:@"Compass" subtext:@"With a built-in digital compass, iPhone 3G S can point the way."] autorelease],
    									 [[[TTSubtextTableField alloc] initWithText:@"Cut, Copy & Paste" subtext:@"Cut, copy, and paste words and photos, even between applications."] autorelease],
    									 nil]] autorelease];
    
    	return dataSource;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)dealloc {
    	[super dealloc];
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewDataSource
    
    - (Class)tableView:(UITableView*)tableView cellClassForObject:(id) object { 
    
    	if ([object isKindOfClass:[TTSubtextTableField class]]) {
    		return [BNSubtextWithRedTitleTableFieldCell class];
    	} else {
    		return [super tableView:tableView cellClassForObject:object];
    	}
    }
    
    - (void)tableView:(UITableView*)tableView prepareCell:(UITableViewCell*)cell
    forRowAtIndexPath:(NSIndexPath*)indexPath {
    	cell.accessoryType = UITableViewCellAccessoryNone;
    }
    
    @end
    

You should end up with something like this when you run your app:

Ordindary cells with red title

Changing a cell’s appearance AND data.

What if we want to add another piece of data to the cell like a URL? Right now the cell can only be fed a title, and subtext because that is all that the cell’s field contains (2 NSStrings).

What we have to do is subclass Three20’s existing TTSubtextTableField (which currently feeds both BNSubtextTableFieldCell and BNSubtextWithRedTitleTableFieldCell) to make a new field which will hold a new piece of data (a NSString for the URL we will add).

But before we make a new field, let’s make a new cell that subclasses our last cell with the addition of a new UILabel for the URL.

  1. To make a new sub-classed cell, change BNCell.h to look like this:

    #import "Three20/Three20.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////
    //////   BNSubtextTableFieldCell     ////
    /////////////////////////////////////////
    we
    @interface BNSubtextTableFieldCell : TTSubtextTableFieldCell {
    
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleTableFieldCell     ////
    /////////////////////////////////////////////////////
    
    @interface BNSubtextWithRedTitleTableFieldCell : BNSubtextTableFieldCell {
    
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ///////////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleAndURLTableFieldCell     ////
    ///////////////////////////////////////////////////////////
    
    @interface BNSubtextWithRedTitleAndURLTableFieldCell : BNSubtextWithRedTitleTableFieldCell {
      UILabel* _featureURLLabel;
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
  2. And change BNCell.m to look like this:

    #import "BNCell.h"
    #import "BNField.h"
    #import "BNDefaultStylesheet.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////
    //////   BNSubtextTableFieldCell     ////
    /////////////////////////////////////////
    
    @implementation BNSubtextTableFieldCell
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)setObject:(id)object {
    	if (_field != object) {
    		[super setObject:object];
    
    		TTSubtextTableField* field = object;
    
    		_label.text = field.text;
    		_label.font = TTSTYLEVAR(myFirstFont);
    		_label.textColor = TTSTYLEVAR(myFirstColor);
    		_label.adjustsFontSizeToFitWidth = YES;
    
    		_subtextLabel.text = field.subtext;
    		_subtextLabel.font = TTSTYLEVAR(mySecondFont);
    		_subtextLabel.textColor = TTSTYLEVAR(mySecondColor);
    	}
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleTableFieldCell     ////
    ////////////////////////////////////t/////////////////
    
    @implementation BNSubtextWithRedTitleTableFieldCell
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)setObject:(id)object {
    	if (_field != object) {
    		[super setObject:object];
    
    		_label.textColor = [UIColor redColor];
    	}
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ///////////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleAndURLTableFieldCell     ////
    ////////////////////////////////////t//////////////////////
    
    @implementation BNSubtextWithRedTitleAndURLTableFieldCell
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    + (CGFloat)tableView:(UITableView*)tableView rowHeightForItem:(id)item {
    	CGFloat maxWidth = tableView.width - 20;
    	BNSubtextWithRedTitleAndURLTableField* field = item;
    
    	CGSize textSize = [field.text sizeWithFont:TTSTYLEVAR(tableSmallFont)
    					   constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
    					   lineBreakMode:UILineBreakModeWordWrap];
    	CGSize subtextSize = [field.subtext sizeWithFont:TTSTYLEVAR(font)
    						  constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX) lineBreakMode:UILineBreakModeWordWrap];
    	CGSize featureURLSize = [field.featureURL sizeWithFont:[UIFont systemFontOfSize:12]
    						  constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX) lineBreakMode:UILineBreakModeWordWrap];
    
    	return 20 + textSize.height + subtextSize.height + featureURLSize.height;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString*)identifier {
    	if (self = [super initWithFrame:frame reuseIdentifier:identifier]) {
    		_featureURLLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    		_featureURLLabel.font = [UIFont systemFontOfSize:11];
    		_featureURLLabel.textColor = [UIColor grayColor];
    		_featureURLLabel.backgroundColor = [UIColor clearColor];
    		_featureURLLabel.highlightedTextColor = TTSTYLEVAR(highlightedTextColor);
    		_featureURLLabel.textAlignment = UITextAlignmentLeft;
    		_featureURLLabel.contentMode = UIViewContentModeTop;
    		_featureURLLabel.lineBreakMode = UILineBreakModeWordWrap;
    		_featureURLLabel.numberOfLines = 0;
    		[self.contentView addSubview:_featureURLLabel];
    	}
    	return self;
    }
    
    - (void)dealloc {
    	[_featureURLLabel release];
    	[super dealloc];
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // UIView
    
    - (void)layoutSubviews {
    	[super layoutSubviews];
    	CGFloat maxWidth = self.contentView.width - 20;
    	CGSize featureURLSize = [_featureURLLabel.text sizeWithFont:_featureURLLabel.font
    						  constrainedToSize:CGSizeMake(maxWidth, CGFLOAT_MAX) lineBreakMode:_featureURLLabel.lineBreakMode];
    	_featureURLLabel.frame = CGRectMake(10, _subtextLabel.bottom + 5, featureURLSize.width, featureURLSize.height);
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewCell
    
    - (void)setObject:(id)object {
    	if (_field != object) {
    		[super setObject:object];
    
    		BNSubtextWithRedTitleAndURLTableField* field = object;
    
    		_featureURLLabel.text = field.featureURL;
    	}
    }
    
    @end
  3. Then we need to feed data into this new cell by sub-classing TTSubtextTableField.

  4. Make a new file called BNField.m (check YES to “Also create BNField.h”).

  5. Then make your new BNField.h file look like this:

    #import "Three20/Three20.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    /////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleAndURLTableField  /////
    /////////////////////////////////////////////////////
    
    @interface BNSubtextWithRedTitleAndURLTableField : TTSubtextTableField {
    	NSString *_featureURL;
    }
    @property(nonatomic, retain) NSString *featureURL;
    - (id)initWithText:(NSString*)text subtext:(NSString*)subtext featureURL:(NSString*)featureURL;
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
  6. And make your BNField.m file look like this:

    #import "BNField.h"
    #import "BNCell.h"
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    ////////////////////////////////////////////////////////
    //////   BNSubtextWithRedTitleAndURLTableField     /////
    ////////////////////////////////////////////////////////
    
    @implementation BNSubtextWithRedTitleAndURLTableField
    
    @synthesize featureURL = _featureURL;
    
    - (id)init {
    	if (self = [super init]) {
    		_featureURL = nil;
    	}
    	return self;
    }
    
    - (id)initWithText:(NSString*)text subtext:(NSString*)subtext featureURL:(NSString*)featureURL {
    	if (self = [super initWithText:text subtext:subtext]) {
    		self.featureURL = featureURL;
    	}
    	return self;
    }
    
    - (void)dealloc {
    	[_featureURL release];
    	[super dealloc];
    }
    
    @end
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////

Then we have to make some changes to RootViewDataSource.m
We will:

  • Include BNField.h
  • Change our datasource method calls from TTSubtextTableField to BNSubtextWithRedTitleAndURLTableField and pass a URL to this new field
  • Change the cellClassForObject method to look for our new cell type (I should note that we have to change the IF statement so that our newest class is on top otherwise it won’t be found).
  1. Make your RootViewDataSource.m look like this:

    #import "RootViewDataSource.h"
    #import "BNCell.h"
    #import "BNField.h"
    
    @implementation RootViewDataSource
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // public
    
    + (RootViewDataSource*)rootViewDataSource {
    	RootViewDataSource* dataSource =  [[[RootViewDataSource alloc] initWithItems:
    									 [NSMutableArray arrayWithObjects:
    									 [[[BNSubtextWithRedTitleAndURLTableField alloc] initWithText:@"Video" subtext:@"Now you can shoot video, edit it, and share it — all on your iPhone 3G S." featureURL:@"http://www.apple.com/iphone/iphone-3gs/video-recording.html"] autorelease],
    									 [[[BNSubtextWithRedTitleAndURLTableField alloc] initWithText:@"3-Megapixel Camera" subtext:@"The new 3-megapixel camera takes great still photos, too, thanks to built-in autofocus." featureURL:@"http://www.apple.com/iphone/iphone-3gs/photos.html"] autorelease],
    									 [[[BNSubtextWithRedTitleAndURLTableField alloc] initWithText:@"Search" subtext:@"Find what you’re looking for across your iPhone, all from one convenient place." featureURL:@"http://www.apple.com/iphone/iphone-3gs/search.html"] autorelease],
    									 [[[BNSubtextWithRedTitleAndURLTableField alloc] initWithText:@"Compass" subtext:@"With a built-in digital compass, iPhone 3G S can point the way." featureURL:@"http://www.apple.com/iphone/iphone-3gs/maps-compass.html"] autorelease],
    									 [[[BNSubtextWithRedTitleAndURLTableField alloc] initWithText:@"Cut, Copy & Paste" subtext:@"Cut, copy, and paste words and photos, even between applications." featureURL:@"http://www.apple.com/iphone/iphone-3gs/cut-copy-paste.html"] autorelease],
    									 nil]] autorelease];
    
    	return dataSource;
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    
    - (void)dealloc {
    	[super dealloc];
    }
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////
    // TTTableViewDataSource
    
    - (Class)tableView:(UITableView*)tableView cellClassForObject:(id) object { 
    
    	if ([object isKindOfClass:[BNSubtextWithRedTitleAndURLTableField class]]) {
    		return [BNSubtextWithRedTitleAndURLTableFieldCell class];
    	} else if ([object isKindOfClass:[TTSubtextTableField class]]) {
    		return [BNSubtextWithRedTitleTableFieldCell class];
    	} else {
    		return [super tableView:tableView cellClassForObject:object];
    	}
    }
    
    - (void)tableView:(UITableView*)tableView prepareCell:(UITableViewCell*)cell
    forRowAtIndexPath:(NSIndexPath*)indexPath {
    	cell.accessoryType = UITableViewCellAccessoryNone;
    }
    
    @end

Once you’re done changing RootViewDataSource.m……

Boom! Your app should look like this when launched:

Custom table cells fed by custom fields.


Comments on this Post

July 27, 2009 - RM

Very cool tutorial… would love to see an image added to the example… :D

August 3, 2009 - jacqueline

Hello,
I tried to follow along with this, but it seems more complicated than what I’m trying to do. I’m using the example from Photos2Controller, and I want to increase the height of the thumbnails (while retaining the width), and also make it 3 columns wide. Do I need to subclass TTThumbsTableViewCell, or is there an easier way to access and change these variables? Any advice would be very much appreciated.

August 10, 2009 - Joe

I have a challenge similar to Jacqueline, and then some. In my original app (without Three20) I have a cell very similar to TTTableSubtitleItem, except the image has a 5px margin around it (it’s really not a margin – I center a 50×50px image within a 60×60 space), and then my text is dynamically sized to 1 or 2 lines (depending on whether or not the text wraps), and then the text and subtitle are both vertically centered in the cell. Now … looking at the code above, I can see how to change the font and color, but I’m not sure where I put the code to do my custom resizing and frame-adjustment. I mean, I see a few possible spots, but that’s the problem – this is where Three20 gets my eyes glazing over. :) Clues welcome/appreciated!

August 10, 2009 - Joe

Actually … I’m even more confused now. Where is myFirstFont/Color (and Second) defined? :-o

August 17, 2009 - MattV

I will be updating this tutorial soon, please check back!

October 9, 2009 - Matt

It took me a few minutes to figure this one out… The newest build of Three20 calls the rowHeight method a little differently from TTTableViewDelegate

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
id dataSource = (id)tableView.dataSource;

id object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
Class cls = [dataSource tableView:tableView cellClassForObject:object];
return [cls tableView:tableView rowHeightForObject:object];
}

Notice that it now says rowHeightForObject, not rowHeightForItem.

Just rename the subclassed method and it will be the right height again

October 9, 2009 - Matt

forget the above comment, it was meant for the newer tutorial

November 2, 2009 - oeyndj

I0pMTZ vngsrfibbocb, [url=http://hgnlqazxdtgs.com/]hgnlqazxdtgs[/url], [link=http://bmgastmfocxg.com/]bmgastmfocxg[/link], http://okzcqixyvgpw.com/


Questions? Comments? Kindly-worded insults?

Thats right, it's all welcome folks! Step right up and leave a your mark for the ages!