Latest in Blog

Missing files

Flex Conditional Compilation Tweak

Bringing Flash to the iPhone?

jEdit OS X startup issue.

Variiable Length Table Cells on the iPhone

More pages in Blog...

 

Variiable Length Table Cells on the iPhone


Want to display long pieces of text in a table on the iphone? Here's how.
2008-09-23

Overview

I've been trying out iPhone development, getting to know the model. Development is quite different, and I will not pretend to be an expert. But I've compiled a couple nuggets out there on the forums into something coherently useful.

One of the challenges I found was that I wanted to dynamically layout text that could be very long. I wanted this text to be available in a table view. The help files explain how to use the UITableViewCell class. There are also tutorials on the web that contain information on subclassing and creating subviews. So this entry will assume that you are familiar with the basics of UITableViewCell and can create a subview.

Setting up our UIViewController

First we must set up our UIViewController to delegate the correct methods for UITableViewDataSource and UITableViewDelegate. Most importantly is the heightForRowAtIndexPath() and cellForRowAtIndexPath()

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSString *text;
	CGSize s;
	UIFont *f;
	text = [self getTextForIndexPath:indexPath];
	f = [UIFont systemFontOfSize:14];
	s = [self GetSizeOfText:text withFont: f];
	return s.height +11;
}

This function calls two custom functions: getTextForIndexPath() and getSizeOfText(). getTextForIndexPath() gets an NSString of the text used for this row and section. I then pass this to a GetSizeOfText() which is explained below. Finally I fudge it with 11px of space. Why 11 pixels? There is 5px on top and bottom of padding, and then 1 extra pixel for the outline on the bottom. Hey, I'm not proud of this code....baby steps.


-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	
	static NSString *identifier = @"MyCell";
	
	DetailViewCell *cell = (DetailViewCell *)[tableView dequeueReusableCellWithIdentifier:identifier];
	if (cell == nil) {
		cell = [[[DetailViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:identifier] autorelease];
	}

	NSString *cellText = [self getTextForIndexPath:indexPath];
	
	[cell setData:cellText];
	
	return cell;
}

This code is very similar to the normal code in cellForRowAtIndexPath(). I include it here for completeness. What we're doing is basically initialising the UITableViewCell for use in drawing the cell. If we have an existing cell, let's reuse it. We then pass the data to the cell using the getTextForIndexPath() function used earlier to also measure the text.

Its important to understand that you cannot assume a correlation between your UITableViewCell and the data within. In other words, the iPhone attempts to efficiently re-use views, and so as it redraws, the data you put in there could be stale. cellForRowAtIndexPath() is your place to initialize your cell with data before it gets drawn.

- (CGSize)GetSizeOfText:(NSString *)text withFont:(UIFont *)font
{
	return [text sizeWithFont: font constrainedToSize:CGSizeMake(280, 500)];
}

I've also added a utility function GetSizeOfText() which returns the size of the text with the font you've specified. One of the curiosities of Cocoa: the string primitive object tells you the bounds of the string if you give it a font and a bounding rectangle (CGRect). I would've never looked there. This is one of those 'delegation idiosynchracies' that occur, since delegates can be placed on whatever you like.

- (void)viewWillAppear:(BOOL)animated
{
	self.title = @"Details";
	[tblView reloadData];
	
}

Another important function to override is the viewWillAppear() delegate. If your view is controlled by a NavigationController (which is pretty likely), it could be that your view will draw correctly the first time, but not reload the data and redraw on a subsequent view.

For example you create a list of news items. You click on an item and go to a subscreen displaying the item's details. You click back and go to another option, yet the view does not redraw. NavigationController's already drawn the view, so it just displays it without updating it. What you want to do is trigger a redraw by overriding the viewWillAppear() delegate and reloading the table view's data via [tblView reloadData];.

Subclassing our UITableViewCell

Ok, so now we have our ViewController set up correctly to report the heights of our variable length cells based upon the text and font we are using. Now we need to create our subclass of UITableViewCell. We'll use the same method used in similar tutorials:

  1. Subclass UITableViewCell
  2. Add subviews to the UITableViewCell's contentView
  3. Override layoutSubviews
  4. Initialize data into our view

Subclass UITableViewCell and add subviews

- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
	if (self = [super initWithFrame:frame reuseIdentifier:reuseIdentifier]) {
		// Initialization code
		// we need a view to place our labels on.
		UIView *myContentView = self.contentView;
		
		//init font.
		UIFont *font = [UIFont systemFontOfSize:14];
		
		// init the label
		self.label = [[UILabel alloc] initWithFrame:CGRectZero];
		self.label.backgroundColor = [UIColor whiteColor];
		self.label.font = font;
		self.label.numberOfLines = 0;
		
		[myContentView addSubview:self.label];
		[self.label release];
		
	}
	return self;
}

Here we grab out contentView from the superclass, and then add a label as a subview.

Override layoutSubviews

-(void)layoutSubviews {
	[super layoutSubviews];
	
	//get the cell size.
	CGRect contentRect = self.contentView.bounds;
	
	if(!self.editing){
		CGFloat boundsX = contentRect.origin.x;
		CGRect frame;
		
		NSString *text = self.label.text;
		UIFont *font = self.label.font;
		CGSize constraint = CGSizeMake(280,500);
		CGSize size = [text sizeWithFont:font constrainedToSize:constraint];
		
		frame = CGRectMake(boundsX + 10, 0, 280, size.height + 10);
		
		self.label.frame = frame;
		
	}
}

What we do here is resize the UILabel to fit within the contentView accurately. You may have noticed that we are doing something similar to GetSizeOfText() within the view controller. In fact its practically duplicate code. We get the size of the text, and then add 10px to the x origin. The 10px amount is because of the rounded rectangle styling of the table. 280px is a good width, there is a 20px margin on each side of the table cell. The height is based upon the CGSize returned from sizeWithFont(). Another 10px is added for styling.

Initialize data into our view

-(void)setData:(NSString *)text {
	self.label.text = text;
}

For these purposes there was no need for any model or data objects within our UITableViewCell. Instead I just set the label's text directly.

Areas for improvement

I'm sure there are some savvy Obj-C programmers out there who can easily pick out some coding flaws here. There is code duplication between the view controller and the view cell. There could be memory leaks. Styling info is hardcoded. But hey, if you want to create a variable length cell, then I hope this code can be useful to you!

DetailViewCell

#import 


@interface DetailViewCell : UITableViewCell {
	UILabel *label;
}


// gets the data from another class
-(void)setData:(NSString *)text;

@property (nonatomic, retain) UILabel *label;

@end



#import "DetailViewCell.h"


@implementation DetailViewCell

@synthesize label;


- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
	if (self = [super initWithFrame:frame reuseIdentifier:reuseIdentifier]) {
		// Initialization code
		// we need a view to place our labels on.
		UIView *myContentView = self.contentView;
		
		//init font.
		UIFont *font = [UIFont systemFontOfSize:14];
		
		// init the label
		self.label = [[UILabel alloc] initWithFrame:CGRectZero];
		self.label.backgroundColor = [UIColor whiteColor];
		self.label.font = font;
		self.label.numberOfLines = 0;
		
		[myContentView addSubview:self.label];
		[self.label release];
		
	}
	return self;
}

-(void)setData:(NSString *)text {
	self.label.text = text;
}

-(void)layoutSubviews {
	[super layoutSubviews];
	
	//get the cell size.
	CGRect contentRect = self.contentView.bounds;
	
	if(!self.editing){
		CGFloat boundsX = contentRect.origin.x;
		CGRect frame;
		
		NSString *text = self.label.text;
		UIFont *font = self.label.font;
		CGSize constraint = CGSizeMake(280,500);
		CGSize size = [text sizeWithFont:font constrainedToSize:constraint];
		
		frame = CGRectMake(boundsX + 10, 0, 280, size.height + 10);
		
		self.label.frame = frame;
		
	}
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {

	[super setSelected:selected animated:animated];

	// Configure the view for the selected state
}


- (void)dealloc {
	[label dealloc];
	[super dealloc];
}


@end

FeedItemController

#import 



@interface FeedItemController : UIViewController 
{
	IBOutlet UITableView *tblView;
	NSDictionary *currentStory;
	UILabel		*textView;
	
}

-(NSString *) testFunction;
- (CGSize)GetSizeOfText:(NSString *)text withFont:(UIFont *)font;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSString *)getTextForIndexPath:(NSIndexPath *)indexPath;

@property (nonatomic, retain) UITableView *tblView;
@property (nonatomic, retain) NSDictionary *currentStory;
@property (nonatomic, retain) UILabel *textView;


@end



#import "FeedItemController.h"
#import "DetailViewCell.h"
#import "UIKit/UITableView.h"
#import 

@implementation FeedItemController

@synthesize tblView, currentStory, textView;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
	if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
		// Initialization code
		NSLog(@"currentStory initWithNibName %@", [currentStory objectForKey:@"summary"]);
	}
	return self;
}

- (void)awakeFromNib
{	
	self.title = @"Details";
	NSLog(@"currentStory awakeFromNib %@", [currentStory objectForKey:@"summary"]);
}

// Implement loadView if you want to create a view hierarchy programmatically
- (void)loadView {
	NSLog(@"currentStory loadView %@", [currentStory objectForKey:@"summary"]);
	//newsSummary.text = @"Interesting";
}
 

 //If you need to do additional setup after loading the view, override viewDidLoad.
- (void)viewDidLoad {
	NSLog(@"currentStory viewDidLoad %@", [currentStory objectForKey:@"summary"]);

}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	
	static NSString *identifier = @"MyCell";
	
	DetailViewCell *cell = (DetailViewCell *)[tableView dequeueReusableCellWithIdentifier:identifier];
	if (cell == nil) {
		cell = [[[DetailViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:identifier] autorelease];
	}
	    
    
	NSString *cellText = [self getTextForIndexPath:indexPath];
	
	//NSLog(@"Setting text to %@", cellText);
	[cell setData:cellText];
	
	return cell;
}

- (NSString *)getTextForIndexPath:(NSIndexPath *)indexPath
{
	switch(indexPath.section){
		case 0:
			switch (indexPath.row){
				case 0:
					return [currentStory objectForKey:@"title"];
					break;
				case 1:
					return [currentStory objectForKey:@"date"];
					break;
			}
			break;                                                                             
		case 1:
			return [currentStory objectForKey:@"summary"];
			break;
		case 2:
			return @"View in Safari";
			break;
			
	}
	return @"";
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
	NSString *text;
	CGSize s;
	UIFont *f;
	text = [self getTextForIndexPath:indexPath];
	f = [UIFont systemFontOfSize:14];
	s = [self GetSizeOfText:text withFont: f];
	//NSLog(@"Width: %f Height: %f", s.width, s.height);
	return s.height +11;
}

- (NSString *)testFunction;
{
	return @"hi";
}

- (CGSize)GetSizeOfText:(NSString *)text withFont:(UIFont *)font
{
	return [text sizeWithFont: font constrainedToSize:CGSizeMake(280, 500)];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
	//boy i need to learn how to use arrays :P
	switch (section) {
		case 0:
			return @"Name and Date";
			break;
		case 1:
			return @"Details";
		default:
			return @"";
			break;
	}
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
	return 3;
}

-(NSInteger)tableView:(UITableView *)tblView numberOfRowsInSection:(NSInteger)section {
	
	//Every cell is going to have two rows.
	switch (section) {
		case 0:
			return 2;
			break;
		case 1:
			return 1;
		default:
			return 1;
			break;
	}
}


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
	// Navigation logic
	if(indexPath.row == 0 && indexPath.section == 2){
		 NSString * storyLink = [currentStory objectForKey: @"link"];
		 
		 // clean up the link - get rid of spaces, returns, and tabs...
		 storyLink = [storyLink stringByReplacingOccurrencesOfString:@" " withString:@""];
		 storyLink = [storyLink stringByReplacingOccurrencesOfString:@"\n" withString:@""];
		 storyLink = [storyLink stringByReplacingOccurrencesOfString:@"	" withString:@""];
		 
		// NSLog(@"link: %@", storyLink);
		 // open in Safari
		 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:storyLink]];
	}
}


#pragma mark UIViewController delegates

- (void)viewWillAppear:(BOOL)animated
{
	//NSLog(@"currentStory viewWillAppear %@", [currentStory objectForKey:@"summary"]);
	//newsSummary.text = [currentStory objectForKey:@"summary"];
	self.title = @"Details";
	[tblView reloadData];
	
}


- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
	// Return YES for supported orientations
	return (interfaceOrientation == UIInterfaceOrientationPortrait);
}


- (void)didReceiveMemoryWarning {
	[super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
	// Release anything that's not essential, such as cached data
}



- (void)dealloc {
	[currentStory release];
	[tblView release];
	[super dealloc];
}




@end
 
 

©2004 Chris Hill. All Rights Reserved.Legal Crapola