Skip to content

Latest commit

 

History

History
291 lines (203 loc) · 10.1 KB

File metadata and controls

291 lines (203 loc) · 10.1 KB

Table views made easy

Most iOS app use table views in a way or another to display lists of data. Objective-C and iOS has changed a lot recently. Table views on the other hand got only a few improvements. Let's discuss the problems UITableView has and a solution to them.

One of the things I disliked about table views is all the boilerplate that comes along:

  • register a nib for each of the cells respective unique identifier
  • return the numbers of sections and cells in every section (witch is ussually the count of an array..)
  • implement tableView:cellForRowAtIndexPath:

All of that so you could add a table view to a controller, and a lot more if you what to enable other features. The worst part is that you have to do the same work all over again the next time you use a table view.

Another thing that bothered me was that the API felt a bit backwards sometimes, like asking for the cell height and after for the cell instead of asking just for the cell.

Lets say you have a list of items(like blog posts) in your app and you want to show them in a table view. This is a common way to implement that:

- (void)viewDidLoad {
    ...
    
    [self.tableView registerNib:[UINib nibWithNibName:@"ItemCell" bundle:nil]
         forCellReuseIdentifier:@"itemCellReuseIdentifier"];

    ...
}

...

- (int)tableView:(UITableView *)tableView numberOfRowsInSection:(int)sectionIndex {
  return self.items.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"itemCellReuseIdentifier"];
  
    Item *item = self.items[indexPath.row];
    [cell loadItem:item];
  
    return cell;
}

Now let's suppose you want to add a ad banner after the second item in the table view. You have to change the code a little in order to do that:

  • increase the number of cells that the table view contains by one.
  • check if the current index is a item or an ad.
  • if it's an ad return the ad cell.
  • otherwise get the correct index of the item, load the item into a ItemCell and then return it.

All of this in order to add a different type of cell to the table view.

- (int)tableView:(UITableView *)tableView numberOfRowsInSection:(int)sectionIndex {
  return self.items.count + 1;
}

...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  if (indexPath.row == 2) {
    AdCell *cell = [tableView dequeueReusableCellWithIdentifier:@"adCellReuseIdentifier"];
    
    [cell loadAd];
    
    return cell;
  } else {
    ItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"itemCellReuseIdentifier"];
  
    Item *item = nil;
    if (indexPath.row < 2) {
      item = self.items[indexPath.row];
    } else {
      item = self.items[indexPath.row - 1];
    }
    [cell loadItem:item];

    return cell;
  }
}

If the cells have different height you have to implement the same "if else if else ..." structure. This affects almost all delegate methods.

If you have multiple types of cells in a table view your code it usually looks like

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (something) {
        // load first cell type
    } else if (something else) {
        // load second cell type
    } else if else if else if .... :(
    
    ...    

    } else {
        // load last cell type
    }
}

Stop writing spaghetti code

What should be fixed:

  • no more "if else if else ..." structures
  • the code related to a section or cell should not change if we add or remove other types in a table view
  • reduce the amount of boilerplate that is required to power up a table view
  • make it as easy as posible to show any kind of list (by default just show the description of an object in the textLabel of the table view cell)
  • code should not change if the order of cells changes
  • it should not be required to create subclasses in order to customize cells

Table Controller

The Controller implements both UITableViewDataSource and UITableViewDelegate protocols and uses section/cell view models to populate the table view. The controller is responsible for creating, reusing and loading the table cell views, also it has an interface for applying changes to the current models and displaying the changes(ex. removing a cell from the table).

@interface APTableController : NSObject <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, weak) IBOutlet UIViewController *viewController;
@property (nonatomic, strong) IBOutlet UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *sections;

- (void)reloadWithData:(id)data;
- (void)reloadTableView:(UITableView *)tableView withData:(NSObject *)data;

- (int)numberOfSections;
- (int)numberOfRowsInSection:(int)sectionIndex;

- (void)realoadTableView;

- (APTableCellViewModel *)cellViewModelAtIndexPath:(NSIndexPath *)indexPath;

- (NSMutableArray *)sectionsFromData:(NSObject *)data;

// Inserting, Deleting, and Moving Rows and Sections

- (void)insertCell:(APTableCellViewModel *)cellViewModel;// in the first section at the end
- (void)insertCell:(APTableCellViewModel *)cellViewModel atIndex:(int)index;//in the first section
- (void)deleteCellAtIndex:(int)index; // in the first section

- (void)insertCell:(APTableCellViewModel *)cellViewModel atIndexPath:(NSIndexPath *)indexPath;
- (void)deleteCellAtIndexPath:(NSIndexPath *)indexPath;

- (void)insertCells:(NSArray *)cells atIndexPaths:(NSArray *)indexPaths;// the cells have to be in the same section
- (void)deleteCellsAtIndexPaths:(NSArray *)indexPaths;

- (void)moveCellAtIndex:(int)index toIndex:(int)toIndex; // first section
- (void)moveCellAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)toIndexPath;

// Sections
- (void)insertSection:(APTableSectionViewModel *)section;
- (void)insertSections:(NSArray *)sections;

- (void)insertSection:(APTableSectionViewModel *)section atIndex:(int)index;
- (void)insertSections:(NSArray *)sections atIndex:(int)index;

- (void)deleteSectionAtIndex:(int)index;
- (void)deleteSectionAtIndexes:(NSArray *)indexes;

- (void)deleteSection:(APTableSectionViewModel *)section;
- (void)deleteSections:(NSArray *)sections;

@end

It has IBOutlets for the view controller and table view so that the integration can be done from interface builder.

Cell and Section Models

The cell model will be resposable for creating a new cell either by registering a nib file or programmatically. The behaviour of the cell can be customized with a few block properties for events like onLoad, onSelect, beforeReuse, that will be called by the controller.

The section view model is similar but it is used to customize the behaviour of a section and its cells. This includes customizing onLoad, onSelect.. for all the cells in the section.

@interface APTableCellViewModel : NSObject

@property (nonatomic, strong) NSString *cellIdentifier;
@property (nonatomic, strong) NSString *nibName;
@property (nonatomic, strong) NSObject *object;

// blocks and magic
@property (nonatomic, copy) APTableViewCellActionBlock onLoad;
@property (nonatomic, copy) APTableViewCellActionBlock beforeReuse;
@property (nonatomic, copy) APTableViewActionBlock onSelect;

// references
@property (nonatomic, weak) UIViewController *viewController;
@property (nonatomic, weak) UITableView *tableView;
@property (nonatomic, strong) NSIndexPath *indexPath;

...

@end

What does this accomplish?

Data normalization

Data normalization is a hack I use when writing components that use a complex data source. It's a convention based on the expected input of a method. For example the table controller method - (void)reloadWithData:(id)data expects to receive a list of section view models to be shown in a table view. Every section has a list of cell view models.

The trick is that by providing partial inputs we can reproduce a "complete" input:

  • If only one section view model is given we can create a list with that one
  • For a list of cell view models we can create a section view model and use the previous logic to complete
  • If a single cell view model is given then we could create a list with that one
  • One could also create a section or cell view model with the information stored in a hash using
+ (instancetype)cellModelWithHash:(NSDictionary *)hash;
  • Every object can be converted into a cell view model using the
+ (instancetype)cellModelWithObject:(id)object;

This is implemented in the following way.

You create a category on NSObject named ConsumerClass that implements - (ExpectedInputClass)asExpectedInput

NSObject+ConsumerClass.h :

@interface NSObject (ConsumerClass)

- (ExpectedInputClass *)asExpectedInput;

@end

NSObject+ConsumerClass.m :

@implementation NSObject (ConsumerClass)

- (ExpectedInputClass *)asExpectedInput {
    return [ExpectedInputClass createWithObject:self];
}

@end

Now if you add a category on _NSArray you could handle different kind of list inputs.

For example this is how APTableController handles list inputs:

@implementation NSArray (APTableController)

- (NSMutableArray *)asTableSectionViewModels {
    BOOL foundArray = NO;
    for (NSObject *object in self) {
        if ([object isKindOfClass:[NSArray class]] == YES) {
            foundArray = YES;
            break;
        }
    }
    
    if (foundArray == YES) {
        return [self mapWithSelector:@selector(asTableSectionViewModel)];
    } else {
        return @[[self asTableSectionViewModel]].mutableCopy;
    }
}

- (APTableSectionViewModel *)asTableSectionViewModel {
    NSArray *cells = [self mapWithSelector:@selector(asCellViewModel)];
    return [APTableSectionViewModel sectionWithCells:cells];
}

@end

This pattern can be combined for multiple data types like cells and section.

Other things

  • The to change the size of a cell all that is needed is to change the frame in the loadModel method.