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:
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 {NSString*caption; NSString*text; } @property(assign)NSString*caption; @property(assign)NSString*text; /* ... */ @end; |
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]; NSString*caption =[self getTitleForIndex:index]; NSString*text =[self getBodyForIndex:index]; return[CaptionAndTextCell cellHeightForCaption:caption text:text width:[[self view] frame].size.width]; } |
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, FLT_MAX) lineBreakMode:UILineBreakModeWordWrap]; CGSize ts =[text sizeWithFont:textFont constrainedToSize:CGSizeMake(width-20.0, FLT_MAX) 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; } |
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]; staticNSString*MyIdentifier =@"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; } |
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]; }} |
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; |
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… ;)