Phillip Caudell

UITableView is dead, long live UITableView

Table views are everywhere. They’re responsible for 99.99% of views you see in iOS (wild, speculative number pulled from my ass). Yet despite their prominent nature on the platform, the API for working with them sucks - and not in a small way, but in a big way.

The imperative API design of UITableView — whilst infinitely extensible — spirals widely out of control for more ambitious layouts. Matching up index paths, calculating cell heights, registering cell instances: it becomes a nightmare. Yes, there’s a special circle in hell for NSIndexPath.

A Quick Story You Can Probably Skip But Shouldn’t So I Can Justify Being So Dramatic

Whilst at 3 SIDED CUBE I worked on the American Red Cross Preparedness apps. The apps have educational material in the form of quizzes and stepped guides, as well a realtime alerting system which notifies you of imminent natural disasters.

The app has a lot of content in a lot of table views built from a lot of different cells. There were text labels that varied in length, inline images, buttons, bullets, ordered lists, check boxes - I could go on.

It became clear it just wasn’t possible to build this natively in the time we had - so we didn’t: we used web views.

They looked and worked pretty well - but they weren’t perfect. They would take a fraction of a second longer to load then their native counterpart, and slip into the uncanny valley of UI. They didn’t feel right. A better solution was needed.

I wanted to build native table views the way I would build up a web view: by defining elements (HTML tags) and defining the elements content. Then I realised what I wanted.

I wanted a declarative API for UITableView.

A Declarative API

Lightning Table provides a declarative API for working with UITableView’s. You can model your table in the same way you would model a HTML document - in that you define your element, then specify the content to go into that element.

If we wanted to build a basic table view with a single row, here’s what it looks like:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.title = @"Basics";
    
    EKTableRow *row = [EKTableRow rowWithTitle:@"Hello World"];
    EKTableSection *section = [EKTableSection sectionWithHeaderTitle:@"Demo" rows:@[row] footerTitle:@"This is the footer of the demo table." selection:^(EKTableRowSelection *selection) {
        NSLog(@"Excellent selection!");
    }];
    
    self.sections = @[section];
}

…and if we were to do that with the imperative API…

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.title = @"Basics";
    
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    
    if (indexPath.row == 0) {
        cell.textLabel.text = @"Hello World";
    }
    
    return cell;
}

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

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 1;
}

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.row == 0) {
        NSLog(@"Excellent choice!");
    }
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return @"Demo";
}

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    return @"This is the footer of the demo table.";
}

As you can see Lightning Table is far more succinct, requiring dramatically less code. We’re not driving a delegate or a datasource, so we don’t need to worry about the how - but instead the what.

All our table logic is in one place — rather than spread out between multiple method calls — meaning we can quickly deduce what the table view’s structure will be without running the app.

Hasta la vista, ba… UITableViewDatasource

The Protocol

Whilst you can create concrete instances of EKTableRow and EKTableSection, Lightning Table’s real power comes with its protocol. Making your objects conform to EKTableRowProtocol means they can be inserted directly into the table view.

Take the below example of a Note’s object.

@implementation EKDNote

+ (instancetype)noteWithText:(NSString *)text
{
    EKDNote *note = [EKDNote new];
    note.text = text;
    note.date = [NSDate date];
    note.color = [UIColor yellowColor];
    
    return note;
}

- (NSString *)rowTitle
{
    return self.text;
}

- (NSString *)rowSubtitle
{
    return self.date.description;
}

- (void)configureRowCell:(EKTableViewCell *)cell
{
    cell.contentView.backgroundColor = self.color;
}

- (Class)rowCellClass
{
    return [EKDNoteTableViewCell class];
}

@end

The object describes how it should be presented in the table view: it specifies the cell title (textLabel), sub title (detailTextLabel) as well what UITableViewCell subclass should be used. The object also has the opportunity to perform manual configuration - in this example the object sets the cell background colour. If you’ve ever used MKAnnotation before, this design should be familiar to you. It means you can have logic contained in small manageable classes, rather than one monolithic table view controller.

Automatic Cell Height Calculation

Another pain point of working with UITableView’s is the need to calculate the cell’s height. For basic layouts with a single line of text, this is usually a fixed value - but for cells with dynamic text lengths this can an iOS developers worst nightmare.

Lightning Table automatically calculates the cells height for you by using a “proxy cell” to perform layout calculations. You don’t need to do anything special (such as using auto layout), this behaviour is automatic and most importantly, it’s fast.

Download on Github

As part of open sourcing Transporter a few weeks ago, I also open sourced Lightning Table. You can find it over on Github.

It’s a little rough around the edges, as it’s a ground up re-write of ThunderTable (the original API used at 3 SIDED CUBE - also checkout Storm), but it’s proved pretty stable so far.

Things currently being worked on:

  • Asynchronous image view support in cells.
  • CocoaPod support.
  • Fix inability to specify UITableViewCellAccessoryType.

Have a play with some of the demo projects, and let me know what you think!

Apple Watch and Apple Retail

Project Transporter