Displaying Variably-Sized Text Cells in a UITableView

For a couple of iPhone apps that i am working on (one is internal, one will hopefully be on the AppStore soon), i needed a flexible way to display text blocks of varying lengths in a list. essentially, i wanted something that looks like this:

This is an ideal candidate for a regular old UITableView, with a custom cell class, and so CaptionAndTextCell was born – a reusable UITableViewCell implementation that is shared by my two apps (and probably many future ones).

So how did i implement this? It’s pretty straight forward. We start by creating a new class descending from UITableViewCell, and call it CaptionAndTextCell. We give it two NSString fields and properties to hold the caption, and text body, respectively:

@interface CaptionAndTextCell : BaseCell {NSStringcaption; NSStringtext; }   @property(assign)NSStringcaption; @property(assign)NSStringtext;   /* ... */   @end;

Besides storing the caption and text, the cell needs to do two core tasks: the obvious one is drawing itself in an overriden -drawRect: method; the second one to actually calculate its size based on the contained text.

Since drawing is difficult without knowing what size to draw at, we’ll start with the second task, and add a new static method to our class, named +cellHeightForCaption:text:width:.

Why a static method? Well, if you’ve looked at the API for UITableView, you will might have noticed that the UITableViewController implementation for your table is responsible for providing the height of individual cells. Because the table view only allocates cells as they need to be displayed, but – for obvious reasons – needs to know the exact height of all cells in advance, we’ll do this calculation in a handy static method hat can be called without actually creating the cell in question. This way, the calculation logic is encapsulated where it should be (inside CaptionAndTextCell), but readily callable. If you ever change how CaptionAndTextCell does its rendering (say, to use a larger font), all your logic is in one place.

All the UITableViewController needs to to to provide the height for a cell is to implement the following method, delegating the actual math to CaptionAndTextCell.

-(CGFloat)tableView:(UITableView )aTableView heightForRowAtIndexPath:(NSIndexPath)indexPath {int index =[indexPath indexAtPosition:1]; NSStringcaption =[self getTitleForIndex:index]; NSStringtext =[self getBodyForIndex:index]; return[CaptionAndTextCell cellHeightForCaption:caption text:text width:[[self view] frame].size.width]; }

(i’m assuming your view controller will have some kind of storage for its data, and -getTitleForIndex: and -getBodyForIndex: will provide the necessary strings).

With that out of the way, we can now implement +cellHeightForCaption:text:width:, to do the actual math.

+(CGFloat)cellHeightForCaption:(NSString)caption text:(NSString)text width:(CGFloat)width; { UIFont captionFont =[UIFont boldSystemFontOfSize:13]; UIFont textFont =[UIFont systemFontOfSize:13]; CGSize cs =[caption sizeWithFont:captionFont constrainedToSize:CGSizeMake(width-20.0, FLTMAX) lineBreakMode:UILineBreakModeWordWrap]; CGSize ts =[text sizeWithFont:textFont constrainedToSize:CGSizeMake(width-20.0, FLTMAX) lineBreakMode:UILineBreakModeWordWrap]; CGSize cs2 =[text sizeWithFont:textFont]; CGSize ts2 =[text sizeWithFont:textFont]; if(ts2.width+cs2.width+15.0 < width)return ts2.height+10.0; return ts.height+cs.height+15.0; }

this method does a couple things. first, we grab copies of the two fonts we want to use to render caption and text, respectively. After all, the final size of the text will depend on the font.

Next, we calculate the pixel size of caption and text (cs and ts – i am keeping variable names short because this code already is hard enough to read online, as it stands), if rendered into a rectangle of width-20.0 (leaving 10 pixels of space around our text, on the sides) with standard word-wrapping. These two will determine the final height of our cell.

Note how the width is never hardcoded anywhere. this way, the cell will work fine in landscape mode, as well as on the fabled 10″ touch when it comes in June (knock on wood ;).

We also calculate the pixel size of our two texts if rendered on a single line (cs2 and ts2). The idea being that if we find both texts actually fit o the same line, we’ll draw them that way, rather than one below the other.

So depending on whether ts2.width and cs2.width fit on one line (along with some white space) or not, we return the appropriate height.

With that done, let’s look go back to our table view controller, and finish hooking up our new cell class. Any table view controller of course needs to implement – tableView:cellForRowAtIndexPath: to provide data. We just need to make a few changes to the default implementation, to create and return our custom cell class and initialize it:

-(UITableViewCell )tableView:(UITableView )tableView cellForRowAtIndexPath:(NSIndexPath)indexPath {int index =[indexPath indexAtPosition:1]; staticNSStringMyIdentifier =@"CaptionAndTextCell"; CaptionAndTextCell cell =(CaptionAndTextCell )[tableView dequeueReusableCellWithIdentifier:MyIdentifier]; if(cell ==nil) cell =[[[RssItemCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease]; [cell setCaption:[self getTitleForIndex:index]]; [cell setText:[self getBodyForIndex:index]]; return cell; }

(once again, you;d replace the calls to -getTitleForIndex: and -getBodyForIndex: with whatever you need to access your data).

We’re almost done; the “only” thing left to do is to tell our cell how to actually draw itself, by overriding -drawRect: as follows:

-(void)drawRect:(CGRect)rect { UIColor captionColor =[UIColor blackColor]; UIColor textColor =[UIColor darkGrayColor]; UIFont captionFont =[UIFont boldSystemFontOfSize:13]; UIFont textFont =[UIFont systemFontOfSize:13]; CGRect f =[self bounds]; CGSize cs =[caption sizeWithFont:captionFont constrainedToSize:CGSizeMake(f.size.width-10, FLT_MAX) lineBreakMode:UILineBreakModeWordWrap]; CGSize cs2 =[caption sizeWithFont:captionFont]; CGSize ts2 =[text sizeWithFont:textFont]; [mainTextColor set]; CGRect c = CGRectMake(5.0, 5.0, f.size.width-10.0, cs.height+5.0); [caption drawInRect:c withFont:captionFont lineBreakMode:UILineBreakModeWordWrap]; [secondaryTextColor set]; if(ts2.width+cs2.width+15.0 < f.size.width-10){ CGPoint p = CGPointMake(cs.width+10.0, 5.0); [text drawAtPoint:p withFont:textFont]; }else{ CGRect r = CGRectMake(5.0, cs.height+10.0, f.size.width-10.0, f.size.height-cs.height-15.0); [text drawInRect:r withFont:textFont lineBreakMode:UILineBreakModeWordWrap]; }}

As before, we obtain copies of the fonts we want to draw in, as well as the colors to use for the individual texts (we’ll draw the caption in black, but the body text in a slightly lighter gray). We also do the same math to obtain the rendered sizes of out text – except we don’t need the ts value this time around – our cell is already sized accordingly, so we can just draw the body text without regard for its actual height.

First we draw the caption, at offset 5/5 of our cell. Next, we check whether caption and body fit on one line. If so, we draw the body text to the right of the caption and are done. If not, we’ll draw it below, word-wrapped to the appropriate width.

Finally, there’s some boilerplate plumbing code, we need to implement property getters and setters, and also initialize and dealloc our cell:

@implementation CaptionAndTextCell -(id)initWithFrame:(CGRect)frame {if(self =[super initWithFrame:frame]){ self.opaque =YES; self.backgroundColor =[UIColor whiteColor]; }return self; }   -(NSString)caption {return caption; }-(void) setCaption:(NSString)aCaption { caption =[[aCaption stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] retain]; [self setNeedsDisplay]; }   -(NSString)text {return text; }-(void) setText:(NSString)aText { text =[[aText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] retain]; [self setNeedsDisplay]; }   -(void)dealloc {[caption release]; [text release]; [super dealloc]; }/* ... */@end;

and with that, we’re done. (in my app, and in the screenshots shown, i also add a slight gradient to the background, to round things off; i’ll save that as exercise for the reader.

I hope you found this a helpful overview on creating a custom UITableViewCell implementation and on measuring and rendering texts. Make also sure to keep an lookout for “FeedOne”, my first and very simplistic app that will use this class, due on the AppStore any month now… ;)

marc hoffman

Chief Architect and CEO here at RemObjects Software. Project Manager for Elements and lead developer of Fire, our awesome new development environment for the Mac.

Curaçao