Posts Tagged ‘UITableView’

Keeping your UITableViewController clean

Saturday, April 3rd, 2010

We’ve all done it, admit it, we’ve all written enormous if structures in the datasource/delegate methods of a table. It got the job done, but ever needed to change the order of the sections or needed to add/remove some rows? Things get out of hand very easily when the appearance and behavior of rows is varying. Indeed, these if structures are horrible maintenance-wise. Granted, tutorials often focus on showing some specific functionality and leave the refactoring to the reader, so aspiring iPhone developers often don’t know any better.

Here’s a way to keep your UITableViewController (or any other view controller or class your using as datasource or delegate) clean, this approach is aimed at tables with multiple sections. Tables with 1 section have limited benefit of using this approach although extra sections could be easily added later. The key is to move all the logic for a specific section in a table to its own class, which is responsible for his own rows and nothing more.

To achieve this we create a protocol called YCSectionController:

@protocol YCSectionController
 
@required
 
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
 
@optional
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
 
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section;
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section;
 
@end

This protocol will be used to specify to which methods a custom section controller must conform. Each custom section controller has knowledge about the amount of rows & how to display each row, these are the required methods. Extra methods can be added like what to do when a row is selected for instance. Depending on the appearance of the section & the desired behavior each custom section controller class implements the necessary methods.

The YCHeaderSectionController implementation below shows a minimal implementation.

@implementation YCHeaderSectionController
 
#pragma mark YCSectionController methods
 
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
	return 1;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	NSString *cellIdentifier = @"HeaderCell";
 
	UITableViewCell *cell = (UITableViewCell*)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (cell == nil) {
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
		cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
	}
 
	if (indexPath.row == 0) {
		cell.textLabel.text = @"Yannick Compernol";
 
	}
 
	return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
	NSURL *url = nil;
 
	if (indexPath.row == 0) {
		url = [NSURL URLWithString:@"http://yannick.compernol.be"];
	}
 
	if (url) {
		[[UIApplication sharedApplication] openURL:url];
	}
}
 
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
	return @"Info";
}
 
@end

One you’ve made several custom section controllers all you have to is to hook them up in your UITableViewController and dispatch the datasource/delegate methods to the custom section controllers. Note the UITableViewControllers has an array which will hold all the custom section controllers, the array is filled in the ViewDidLoad: method.

@interface MainTableViewController : UITableViewController {
	NSArray *sectionControllers;
}
@end
 
@implementation MainTableViewController
 
...
 
#pragma mark UIViewController methods
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
	id header = [[YCHeaderSectionController alloc] init];
	id content = [[YCContentSectionController alloc] init];
	id footer = [[YCFooterSectionController alloc] init];
 
	sectionControllers = [[NSArray alloc] initWithObjects:header, content, footer, nil];
 
	[header release];
	[content release];
	[footer release];
}
 
...
 
#pragma mark UITableViewDataSource methods
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
	return [sectionControllers count];
}
 
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
	id<YCSectionController> sectionController = [sectionControllers objectAtIndex:section];
	return [sectionController tableView:table numberOfRowsInSection:section];
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	id<YCSectionController> sectionController = [sectionControllers objectAtIndex:indexPath.section];
	return [sectionController tableView:tableView cellForRowAtIndexPath:indexPath];
}
 
...
 
#pragma mark UITableViewDelegate methods
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
	id<YCSectionController> sectionController = [sectionControllers objectAtIndex:indexPath.section];
	if ([sectionController respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
		[sectionController tableView:tableView didSelectRowAtIndexPath:indexPath];
	}	
}
 
@end

And that is about it, it can be taken further than I showed here, but I think this is a decent starter. The bottom line is that the UITableViewController has less code and the specific code (appearance & behavior) for each section is contained in its own class. Adding to the maintainability of the whole table.

A running example (screenshot shown above) can be downloaded here.

There’s only one thing I’m still struggling with, since id does not have a respondsToSelector: method, you’ll get a warning that respondsToSelector: wasn’t found in the protocol. An easy fix would be to add it to the protocol, but that doesn’t feel right as it’s not part of the behavior I wanted to encapsulate in the section controller. I’d love to hear your thoughts/comments on this!