Can’t add self as subview

tl;dr – see the Conclusion at the bottom.

I too have been getting occasional crash reports through from users regarding this, and then today suffered the indignity of it for myself. This was caused when selecting a row on my table view attempted to push a new nav controller, and then I pressed the back button. Strangely the nav controller I pushed contained no data. After pressing back, the view under the nav bar went black and the app crashed.

Looking around, all I could find was other users (here, here and here) suggesting this was down to calling a segue or pushing a view controller twice in rapid succession. However, I call pushViewController: from within my tableView:didSelectRowAtIndexPath: only once. Short of the user double tapping on a cell, I don’t see why this could happen to me. Testing by double tapping a table view cell failed to reproduce the observed crash.

However, what if the user had double tapped because the main thread happened to be blocked at the time? (I know, I know, never ever block the main thread!)

To test this, I created some (rather horrific, sorry, quickly cut and paste from other code) class methods (in the fictional class MyDebugUtilities) to repeatedly block and unblock the main thread, launching it just before I opened the view controller containing the table view that caused the crash. Here’s the code for anyone who wants to quickly check this for themselves (a call to [MyDebugUtilities repeatedlyBlockTheMainThread]; will block the main thread for 3 seconds using a semaphore, then queue up another call to repeatedlyBlockTheMainThread 3 seconds later, ad infinitum. BTW, you don’t want to launch this method repeatedly or it’ll end up just blocking all the time):

+ (void)lowCostSemaphoreWait:(NSTimeInterval)seconds
{
    // Use a semaphore to set up a low cost (non-polling) delay on whatever thread we are currently running
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC);

    dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_signal(semaphore);
    });

    NSLog(@"DELAYING...");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"END of delay\n\n");
}


+ (void)repeatedlyBlockTheMainThread
{    
    dispatch_async(dispatch_get_main_queue(), ^{
        // Block the main thread temporarily using a semaphore
        [MyDebugUtilities lowCostSemaphoreWait:3.0];

        // Queue up another blocking attempt to happen shortly
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
        dispatch_after(delayTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [MyDebugUtilities repeatedlyBlockTheMainThread];
        });
    });
}

Putting a NSLog call in my tableView:didSelectRowAtIndexPath:, and then attempting to double tap in one of the time periods when the main thread was blocked did indeed reproduce the bug as observed above – pressing back crashed the app, and from the NSLog it was obvious tableView:didSelectRowAtIndexPath: was getting called twice. I think here what is happening is that the touches are getting queued up while the main thread is blocked, and then delivered all-together as soon as it is freed up. Double-tapping when the main thread is not blocked does not generate two calls to tableView:didSelectRowAtIndexPath:, presumably because it’s already processed the first touch and handled the push.

Because this requires the main thread to be blocked temporarily, and in a well crafted app this should happen rarely (if at all), this would explain the fact that this is really hard to reproduce – I’ve had crash reports for this from a tiny percentage of users, and because the crash reports didn’t even indicate which of my view controllers had triggered the scenario, I had no idea of where to start looking until I experienced it for myself.

To solve this, the really simple solution is to create a BOOL property (selectionAlreadyChosen say), which you set to NO in viewWillAppear: (so when you go back to this screen it gets reset), and then implement tableView:willSelectRowAtIndexPath: to do something like:

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.selectionAlreadyChosen) {
        NSLog(@"BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath:");
        return nil;
    }

    self.selectionAlreadyChosen = YES;
    return indexPath;
}

Not forgetting to remove any NSLogs and the call to repeatedlyBlockTheMainThead when you’re done.

This exact scenario might not be the problem everyone else is experiencing, but I think there’s a problem in the way Apple handles simultaneous multiple table view selections – it isn’t just double-taps – testing the above solution by wildly tapping on the same cell when the main thread was blocked, generated a stream of BLOCKING duplicate calls to tableView:didSelectRowAtIndexPath: messages.

Actually, testing this on iOS 6 – the multiple calls to tableView:didSelectRowAtIndexPath: were not prevented, but these were handled differently (this being the result of calling pushViewController: repeatedly of course!) – on iOS 6 multiple taps while the main thread was blocked would give you the message:

Warning: Attempt to dismiss from view controller
while a presentation or dismiss
is in progress!

just before being unceremoniously dumped out of that UINavigationController. So it’s maybe not so much that this is an iOS 7 bug, but that the handling has changed under the covers, and a problem that was always there is now manifesting as a crash.

Update:

Going through my project looking for other tableView’s that exhibit this problem, I’ve found one that doesn’t. The only differences I can see are that this working one is editable (so supports switching in and out of edit mode, moving rows, and swipe to delete), and is closer to the root of my view hierarchy. The one that had been failing above was not editable, and is way down the view hierarchy – appearing after a number of modal segues. I can’t see any other major differences between them – they both share a common superclass – but one attempts to pushViewController: repeatedly without the above fix, and the other doesn’t.

Interestingly, the one that doesn’t exhibit the problem also calls performSegueWithIdentifier: instead of pushViewController: when it is in editing mode, and when this is the case, it DOES repeatedly call tableView:didSelectRowAtIndexPath: (and hence performSegueWithIdentifier:) – but something about calling pushViewController: seems to cancel the repeated calls to tableView:didSelectRowAtIndexPath:.

Confused? I know I am. Trying to break the ‘working’ view controller by making it non-editable does nothing. Replacing the non-edit mode pushViewController: with one of the performSegueWithIdentifier: caused the segue to be called multiple times, so it isn’t anything to do with being in edit mode in the tableView.

Replacing the working view controller in the hierarchy with the one that doesn’t (so that it was linked by rootViewController relationships rather than modal segues) FIXED THE PREVIOUSLY BROKEN VIEW CONTROLLER (with the previous fix taken out of it).

Conclusion – there’s something about how your view hierarchy is wired up, and maybe down to using Storyboards, or modal segues, or maybe I’ve a broken view hierarchy that causes this – that causes multiple taps on a table view cell to be sent all at once if your main thread happens to be blocked at the instant the user double taps. If you are calling pushViewController: within your tableView:didSelectRowAtIndexPath: it will end up being called multiple times. On iOS 6 this will dump you out of the offending view controller. On iOS 7 this will cause a crash.

Leave a Comment