30

I am using a UITableView to present a variety of viewControllers. Depending on which index is clicked, it will present a different view controller, which can then be dismissed. For some reason one of my ViewControllers takes two clicks to display. Thus, it seems every other click is working. Other view presentations work okay, so the table is probably okay.

What I see in the ViewController about to be displayed is that on the first click, it makes it through didSelectRowAtIndexPath and then fires in the target view: ViewWillLoad, ViewLoaded, ViewWillAppear.... but not ViewDidAppear. On the second click, only ViewDidAppear will fire as it displays. On the second click it doesn't even go through didSelectRowAtIndexPath. Also, I can click anywhere on the screen on the second time and it will display.

It will continue to do this back and forth, seemingly only displaying the target view every other click. Any thoughts why this is occuring?

/* RightPanelViewController */
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
switch(indexPath.row){
    case 0:
        // this view takes two clicks to display
        [self presentViewController:self.delegateRef.savedRidesViewController animated:YES completion:nil];
        break;
    case 1:
        // this works fine, as do others
        [self presentViewController:self.delegateRef.sensorsViewController animated:YES completion:nil];
        break;
    case 2:
       ...
}

/* savedRidesViewController */
- (void) viewWillAppear:(BOOL)animated {
   [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated{
   [super viewDidAppear:animated];
}
Miro
  • 5,307
  • 2
  • 39
  • 64
  • 1
    Could you add the code that initialize the ViewController? – dcorbatta Dec 02 '13 at 04:05
  • After further testing, I was able to determine it wasn't the presented viewcontroller itself. It was happening for multiple unrelated views. Found another solution posted below. – Miro Dec 05 '13 at 18:02
  • I have the same problem. Seems like an iOS7 bug - doesn't happen on iOS6. Also, in my case it happens only the first time after I open the presenting view controller. – AXE Mar 04 '14 at 13:27

8 Answers8

57

Check this out: https://devforums.apple.com/thread/201431 If you don't want to read it all - the solution for some people (including me) was to make the presentViewController call explicitly on the main thread:

dispatch_async(dispatch_get_main_queue(), ^{
    [self presentViewController:myVC animated:YES completion:nil];
});

Probably iOS7 is messing up the threads in didSelectRowAtIndexPath.

AXE
  • 8,335
  • 6
  • 25
  • 32
  • Your suggestion worked for me. But can you please what could be the reason of delay as I am working on main thread only then what is the need to call for main thread? – user2955351 Oct 30 '14 at 06:47
  • I don't know. Just saw this on the dev forums. My guess is that although you don't use other threads, the UITableView's methods do and call didSelectRowAtIndexpath: from such a thread so everything you do inside is also on it. This can easily be checked with a breakpoint. Tell us if you do :) – AXE Oct 30 '14 at 09:15
  • 2
    The reason why this works is that something (probably the table view) does some work on the main thread after didSelect... is called that somehow cancels the presentation of the view controller. Calling the main queue asynchronously allows the run loop to complete (and the table view to finish what it's doing) and then we can present the view controller. It's a smelly mess. – Mikkel Selsøe Oct 21 '15 at 14:04
  • 1
    still happens in iOS9.3 – marosoaie Oct 21 '16 at 17:44
  • Lots of posts about this. Don't think it's a 'non-main thread' issue (you can see its all happening on the main thread). I think Mikkel Selsøe has it about right. It's something to do with the action not triggering due to no event. Calling `deselectRow` (like someone suggested as a 'magic' fix) simply triggers a refresh. Not sure a dispatch from the already main thread is the best way. – bauerMusic Sep 09 '17 at 13:53
  • 2
    This is still the case on iOS 11. I agree it's not an issue of the thread per se, it's a timing issue. `DispatchQueue.main.async { }` will start a task on the next runloop thus allowing the tableView finish its thing first. – Au Ris May 02 '18 at 07:09
6

After much more debugging, I was able to determine it wasn't the view controller, but something to do with the the didSelectRowAtIndexPath and presentViewController. It started happening to other views I was presenting also, and not reliably. I also tried using if/else instead of switch with no help.

Ultimately I found this solution was able to fix it, but I'm not entirely sure why. By using the performSelector with no delay, all the views load instantly (perhaps faster than without it?) and reliably every time.

It isn't as clean of code, but it does reliably work now. Am still curious why this is needed.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {    
    switch(indexPath.row){
        case 0:
            [self performSelector:@selector(load0:) withObject:nil afterDelay:0];
            break;
        case 1:
            [self performSelector:@selector(load1:) withObject:nil afterDelay:0];
            break;
        case 2:
            [self performSelector:@selector(load2:) withObject:nil afterDelay:0];
            break;
        ...
    }
}

-(void)load0:(id)sender {
     [self presentViewController:self.delegateRef.zonesViewController animated:YES completion:nil];
}
-(void)load1:(id)sender {
     [self presentViewController:self.delegateRef.sensorsViewController animated:YES completion:nil];
}
-(void)load2:(id)sender {
     [self presentViewController:self.delegateRef.savedRidesViewController animated:YES completion:nil];
}
Miro
  • 5,307
  • 2
  • 39
  • 64
  • I have same problem. I had to do the same as you. But I'd like to know the reason why is this happening. Any idea? – Michal Jan 24 '14 at 16:53
  • I've not figured it exactly why yet. Anyone else out there? – Miro Jan 24 '14 at 17:47
  • `performSelector:withObject:afterDelay:` takes slightly longer than calling the method directly. That is why your losing focus event and start editing event suddenly get fired in the right order. I would recommend to set the afterDelay parameter to `.1` to make sure. – Daniel Jun 16 '14 at 13:06
  • This bug is not fixed yet on iOS 8. I cannot believe. – Robasan Nov 24 '14 at 10:13
5

This is an actual bug in presentViewController:animated:completion:. See my answer with the proper workaround here: https://stackoverflow.com/a/30787046/1812788

Community
  • 1
  • 1
Tamás Zahola
  • 9,271
  • 4
  • 34
  • 46
5

Swift 3 :

DispatchQueue.main.async(execute: {
   self.present(yourViewControllerHere, animated: true, completion: nil)
})
Phil
  • 4,730
  • 1
  • 41
  • 39
3

In Swift and iOS9 it's:

dispatch_async(dispatch_get_main_queue(), {
    self.performSegueWithIdentifier("OpenBookingDetail", sender: self)
})
jobima
  • 5,790
  • 1
  • 20
  • 18
  • `DispatchQueue.main.async { self.performSegueWithIdentifier("OpenBookingDetail", sender: self) }` ( swift 3) – Gonzo Oin May 24 '17 at 13:36
2

As already mentioned, one fix is to run any presenting of the view controllers on the main thread explicitly or to use performSelector.

However, it wasn't clear to me why this was a fix because I was under the impression that didSelectRow runs on the main thread anyway. It wasn't until I read this answer that things became clear for me.

For details, read the linked answer, but in short it seems that this affects cells that have a selection style of none. This turns off the (by default grey) animation that occurs when a cell is selected which means that the run loop is not triggered for when didSelectRow is called. So when you go to present another view controller the animation that actually presents it doesn't immediately occur on the run loop.

The solution of simply running it on the main thread is not a direct fix. You actually just need to trigger the run loop, such as by supplying it with some code and it will work. For example:

dispatch_async(dispatch_get_main_queue(), {}) [self presentViewController:viewController animated:YES completion:nil]

will also fix it. This has the side effect of triggering the run loop when the view controller is presented so the animation of the view controller now happens instantly. This is also why calling performSelector works.

Gordonium
  • 3,389
  • 23
  • 39
0

One reason for this behavior could be that your table view code, and thus the code to start up your other view controllers, is not executed on the main thread. But all code related to UI must be executed on the main thread to work correctly.
Thus please ensure that the code runs on the main thread, e.g. by setting an appropriate break point.

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
0

At the end of didSelectRow method, deselect it so that it's available for selection the next time.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 .
 .
 .
 tableView.deselectRow(at: indexPath, animated: false)
}
iminiki
  • 2,549
  • 12
  • 35
  • 45
user8637139
  • 43
  • 1
  • 3