UITableViewCell Is Not a Controller – Solution

I’ve been unhappy with how Apple uses UITableViewCells for a while; more so lately when using NSFetchedResultsController because something has to retain the managed objects to prevent them being faulted, which Apple solves in its subclasses by retaining them in the UITableViewCell subclass, which in my opinion is a bit of a step too far in the wrong direction from the usual configureWithObject: method. In researching this problem I came across this post UITableViewCell Is Not a Controller which makes a lot of sense so thought I would design a solution for it. My idea is to use a view controller to manage the cell view and then use the container APIs for retaining the array of controllers and also make viewWillAppear etc. work. Start with the built-in Master-View template with Core Data enabled and make the following changes.

First we create our View Controller class that will be used for the table cells:

@interface EventTableViewCellController : UIViewController

@property (nonatomic, strong) Event *event;
@property (nonatomic, strong) UITableViewCell *view;
@property (nonatomic, strong) UITableViewController *parentViewController;

@end

@implementation EventTableViewCellController
@dynamic view, parentViewController;

- (void)viewDidLoad{
    // isn't called when view is set externally like we do now
}

- (void)loadView{
    // could implement a default view
    // self.view = [self.parentViewController.tableView dequeueReusableCellWithIdentifier:@"Cell"]; // forIndexPath could be implemented via a property.
}

- (void)viewWillAppear:(BOOL)animated{
    self.view.textLabel.text = self.event.timestamp.description;
}

@end

Second we use the controller class in the MasterViewController as follows:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    EventTableViewCellController *vc = [EventTableViewCellController.alloc init];
    if(!vc){
        vc = [EventTableViewCellController.alloc init];
        vc.view = cell;
    }
    vc.event = [self.fetchedResultsController objectAtIndexPath:indexPath];
    [self addChildViewController:vc];
    return cell;
}

// viewWillAppear is called just before this.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
    EventTableViewCellController *vc = [UIViewController viewControllerForView:cell];
    [vc didMoveToParentViewController:self];
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath{
    EventTableViewCellController *vc = [UIViewController viewControllerForView:cell];
    if(!vc.parentViewController){
        return; // handle the long-standing didEndDisplayingCell called twice bug.
    }
    [vc willMoveToParentViewController:nil];
    [cell removeFromSuperview]; // viewWillDisappear is called after this. Normally cells are just set hidden so removing may slow an optimisation.
    [vc removeFromParentViewController];
}

Note, you’ll need a way to get the view controller from the view which can be done in various ways but for demo purposes here is Apple’s private API:

@interface UIViewController()
+ (__kindof UIViewController *)viewControllerForView:(UIView *)view;
@end

Currently the view is set on the cell controller from the table controller, however a more complete implementation could create dequeue the cell inside the view controller, which has the advantage that viewDidLoad will be called, and the cellForRow is a bit simpler because it can return vc.view.