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;


@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;


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];
        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];
        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;

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.

Default behaviour of UISplitViewController collapseSecondaryViewController

The documentation for UISplitViewControllerDelegate collapseSecondaryViewController says:

When you return NO, the split view controller calls the collapseSecondaryViewController:forSplitViewController: method of the primary view controller, giving it a chance to do something with the secondary view controller’s content. Most view controllers do nothing by default but the UINavigationControllerclass responds by pushing the secondary view controller onto its navigation stack.

Similarily, for separateSecondaryViewControllerFromPrimaryViewController it says:

When you return nil from this method, the split view controller calls the primary view controller’s separateSecondaryViewControllerForSplitViewController: method, giving it a chance to designate an appropriate secondary view controller. Most view controllers do nothing by default but the UINavigationControllerclass responds by popping and returning the view controller from the top of its navigation stack.

I thought it might be interesting to try to implement this magic behaviour to help understand what is going on and thus provide a starting point for customisation.

- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
    if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]] && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)) {
        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;
    } else {
        // push the secondary view controller onto the primary's navigation stack
        UINavigationController *nav = (UINavigationController *)primaryViewController;
        [nav pushViewController:secondaryViewController animated:NO];
        return YES;


- (nullable UIViewController *)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController{

    // respond by popping and returning the view controller from the top of its navigation stack
    UINavigationController *nav = (UINavigationController *)primaryViewController;
    return [nav popViewControllerAnimated:NO];


It’s strange it is possible to push the secondary navigation controller , since usually that exceptions with “Pushing a navigation controller is not supported”. Turns out within the push method it checks a private property _allowNestedNavigationControllers to allow it to pass in this case, and it must have been set by the split view controller at some point. These kind of tricks are really annoying because gives inconsistent behaviour and thus lowers developer confidence in the APIs.
It’s interesting to call: [(UINavigationController *)secondaryViewController setViewControllers:nil]; before pushing because it throws an exception that proves that nested navigation controllers are being used: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot display a nested UINavigationController with zero viewControllers

I find using the plus iPhone simulator useful for testing the split controller because in portrait it is compact width but rotating to landscape it moves to regular width.