UITableViewCells

One of the biggest mistakes Apple made in the design of the iOS API was to expose the component views within a UITableViewCell.

Okay, so it really wasn’t Apple’s fault; they wanted to solve the problem of providing access to the components of a table view cell so we could use the off-the-shelf parts easily in our application. By exposing the textLabel, detailTextLabel and imageView UIView components, it makes it easy for us to modify the off-the-shelf parts to our own needs, providing a look which is consistent with the rest of the iOS universe.

But it was a mistake because it taught an entire generation of iOS developers some really bad habits.


One of the things that I greatly appreciate about object oriented programming is that it allows us to easily design applications as a collection distinct separate objects with well-defined purposes or tasks. This separation of concerns permits us to create modular classes, which have the following advantages:

  • Maintainable code. By isolating components into well-defined objects, a modification that needs to be made to one section of the code will, in a worse case scenario, require minor modifications in some other isolated areas of the code. (This, opposed to “spaghetti code” where one change ripples throughout the entire application.)
  • Faster development. Modules which are required by other areas of the application but which haven’t been developed yet can be “mocked up” in the short term to permit testing of other elements of the code.
  • Simplified testing. Individual modules can be easily tested and verified as correct within their limited area of concern. Changes that need to be made to fix a bug in an individual module or object can be made without major changes in the rest of the application.

There are others, but these are the ones I rely upon on a near daily basis as I write code.

Now why do I believe Apple screwed up?

Because it discourages the creation of complex table views (and, by extension, complex view components in other areas of the application) as isolated and well-defined objects which are responsible for their own presentation, and instead encourages “spaghetti code” in the view controller module.

Here’s a simple example. Suppose we want to present a table view full of stock quotes, consisting of the company name, company ticker symbol, current quote and sparkline–a small graph which shows the stock’s activity for the past 24 hours.

Suppose we code our custom table cell in the way that Apple did: by creating our custom view, custom table layout–and then exposing the individual elements to the table view controller.

Our stock data object looks like:

@interface GSStockData : NSObject

@property (readonly) NSString *ticker;
@property (readonly) NSString *company;
@property (readonly) double price;
@property (readonly) NSArray *history;
...
@end

This would give us a stock table view cell that looks like this:

#import 
#import "GSSparkView.h"

@interface GSStockTableViewCell : UITableViewCell

@property (strong) IBOutlet UILabel *tickerLabel;
@property (strong) IBOutlet UILabel *companyLabel;
@property (strong) IBOutlet UILabel *curQuoteLabel;
@property (strong) IBOutlet GSSparkView *sparkView;

@end

And our table view controller would look like this:

#import "GSStockTableViewController.h"
#import "GSStockData.h"
#import "GSStocks.h"
#import "GSStockTableViewCell.h"

@interface GSStockTableViewController ()

@end

@implementation GSStockTableViewController

#pragma mark - Table view data source

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[GSStocks shared] numberStocks];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	GSStockTableViewCell *cell = (GSStockTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"StockCell" forIndexPath:indexPath];

	GSStockData *data = [[GSStocks shared] dataAtIndex:(int)indexPath.row];

	cell.tickerLabel.text = data.ticker;
	cell.companyLabel.text = data.company;
	cell.curQuoteLabel.text = [NSString stringWithFormat:@"%.2f",data.price];
	[cell.sparkView setValueList:data.history];

    return cell;
}

@end

Seems quite reasonable, and very similar to what Apple does. No problems.

Until we learn that the API has changed, and now in order to get the stock price history for our stock, we must call an asynchronous method which queries a remote server for that history. That is, instead of having a handy history of stocks in ‘history’, instead we have something like this:

#import 
#import "GSStockData.h"

@interface GSStocks : NSObject

+ (GSStocks *)shared;

- (int)numberStocks;
- (GSStockData *)dataAtIndex:(int)index;

- (void)stockHistory:(int)index withCallback:(void (^)(NSArray *history))callback;
@end

That is, in order to get the history we must obtain it asynchronously from our stock API.

What do we do?

Well, since the responsibility for populating the table data lies with our view controller, we must, on each table cell, figure out if we have history, pull the history if we don’t have it, then refresh the table cell once the data arrives.

So here’s one approach.

(1) Add an NSCache object to the table view controller, and initialize in viewDidLoad:

@interface GSStockTableViewController ()
@property (strong) NSCache *cache;
@end

@implementation GSStockTableViewController

- (void)viewDidLoad
{
	self.cache = [[NSCache alloc] init];
}

(2) Pull the history from the cache as we populate the table contents. If the data is not available, set up an asynchronous call to get that data.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	GSStockTableViewCell *cell = (GSStockTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"StockCell" forIndexPath:indexPath];

	GSStockData *data = [[GSStocks shared] dataAtIndex:(int)indexPath.row];

	cell.tickerLabel.text = data.ticker;
	cell.companyLabel.text = data.company;
	cell.curQuoteLabel.text = [NSString stringWithFormat:@"%.2f",data.price];

	/*
	 *	Determine if we have the contents and if not, pull asynchronously
	 */

	NSArray *history = [self.cache objectForKey:@( indexPath.row )];
	[cell.sparkView setValueList:history];		// set history
	if (history == nil) {
		/* Get data for cache */
		[[GSStocks shared] stockHistory:data withCallback:^(NSArray *fetchedHistory) {
			/*
			 *	Now pull the cell; we cannot just grab the cell from above, since
			 *	it may have changed in the time we were loading
			 */

			GSStockTableViewCell *stc = (GSStockTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
			if (stc) {
				[stc.sparkView setValueList:fetchedHistory];
			}
			[self.cache setObject:fetchedHistory forKey:@( indexPath.row )];
		}];
	}

    return cell;
}

Notice the potential problems that can happen here. For example, if a user didn’t understand that a cell may be reused by the tableview, the wrong sparkline could be placed in the tableview if the user scrolls rapidly:

	NSArray *history = [self.cache objectForKey:@( indexPath.row )];
	[cell.sparkView setValueList:history];		// set history
	if (history == nil) {
		/* Get data for cache */
		[[GSStocks shared] stockHistory:data withCallback:^(NSArray *fetchedHistory) {
			/*
			 *	Now pull the cell; we cannot just grab the cell from above, since
			 *	it may have changed in the time we were loading
			 */

			// The following is wrong: the cell may have been reused, and this
			// will cause us to populate the wrong cell...
			[cell.sparkView setValueList:fetchedHistory];
			[self.cache setObject:fetchedHistory forKey:@( indexPath.row )];
		}];
	}

And that’s just one asynchronous API with a simple interface. How do we handle errors? What if there are multiple entry points? What if other bits of the code is using the table view?

Now if we had put all the responsibility for displaying the stock quote into the table view cell itself:

#import 
#import "GSStockData.h"

@interface GSStockTableViewCell : UITableViewCell

- (void)setStockData:(GSStockData *)data;

@end

Then none of the asynchronous calling to get values and properly refreshing the cells is the responsibility of the table view cell. All it has to do is:

#pragma mark - Table view data source

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[GSStocks shared] numberStocks];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	GSStockTableViewCell *cell = (GSStockTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"StockCell" forIndexPath:indexPath];

	GSStockData *data = [[GSStocks shared] dataAtIndex:(int)indexPath.row];
	[cell setStockData:data];

    return cell;
}

@end

This is much cleaner. And for our table cell, because it has all the responsibility of figuring out how to populate the contents, if we have to rearrange the cell, it has zero impact on our table view controller.

Our table view cell becomes relatively straight forward as well:

#import "GSStockTableViewCell.h"
#import "GSSparkView.h"
#import "GSStocks.h"

@interface GSStockTableViewCell ()
@property (strong) GSStockData *currentData;

@property (strong) IBOutlet UILabel *tickerLabel;
@property (strong) IBOutlet UILabel *companyLabel;
@property (strong) IBOutlet UILabel *curQuoteLabel;
@property (strong) IBOutlet GSSparkView *sparkView;
@end

@implementation GSStockTableViewCell

- (void)setStockData:(GSStockData *)data
{
	self.tickerLabel.text = data.ticker;
	self.companyLabel.text = data.company;
	self.curQuoteLabel.text = [NSString stringWithFormat:@"%.2f",data.price];

	/*
	 *	If the history was in our object then we'd write the following:
	 */

	// [self.sparkView setValueList:data.history];

	/*
	 *	Instead we get it through a call to our data source
	 */

	self.currentData = data;	/* Trick to make sure we still want this data */
	[[GSStocks shared] stockHistory:data withCallback:^(NSArray *fetchedHistory) {
		/*
		 *	Verify that the history that was returned is the same as the
		 *	history we're waiting for. (If another call to this is made with
		 *	different data, self.currentData != data, as data was locally
		 *	cached in our block.
		 */

		if (self.currentData == data) {
			[self.sparkView setValueList:fetchedHistory];
		}
	}];
}

@end

And what about our caching?

Well, if we’re using proper separation of concerns, we’d create a new object which was responsible for caching the data from our API. And that has the side effect of being usable everywhere throughout the code.


My point is simple: unless you are using the canned UITableViewCell, make your table view cell responsible for displaying the contents of the record passed to it. Don’t just expose all of the internal structure of that table cell to your view controller; this also pushes responsibility for formatting to the view controller, and that can make the view controller a tangled mess.

Make each object responsible for one “thing”: the table view cell is responsible for displaying the data in the stock record, including fetching it from the back end. The view controller simply hands off the stock record to the table view cell. A separate class can be responsible for data caching. And so forth.

By making each object responsible for it’s “one thing”, it makes dealing with changes (such as an asynchronous fetch of history) extremely easy: instead of having to modify a view controller which slowly becomes extremely overloaded, we simply add a few lines of code to our table view cell–without actually expanding our class’s “responsibility” for displaying a single stock quote.


And in the end you’ll reduce spaghetti code.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s