HalfNormalled HalfNormalled - 6 months ago 62
iOS Question

presentViewController:animated:YES view will not appear until user taps again

I'm getting some strange behaviour with

presentViewController:animated:completion
. What I'm making is essentially a guessing game.

I have a
UIViewController
(frequencyViewController) containing a
UITableView
(frequencyTableView). When the user taps on the row in questionTableView containing the correct answer, a view (correctViewController) should be instantiate and its view should slide up from the bottom of the screen, as a modal view. This tells the user they have a correct answer and resets the frequencyViewController behind it ready for the next question. correctViewController is dismissed on a button press to reveal the next question.

This all works correctly every time, and the correctViewController's view appear instantly as long as
presentViewController:animated:completion
has
animated:NO
.

If I set
animated:YES
, correctViewController is initialized and makes calls to
viewDidLoad
. However
viewWillAppear
,
viewDidAppear
, and the completion block from
presentViewController:animated:completion
are not called. The app just sits there still showing frequencyViewController until I make a second tap. Now, viewWillAppear, viewDidAppear and the completion block are called.

I investigated a bit more, and it's not just another tap that will cause it to continue. It seems if I tilt or shake my iPhone this can also cause it to trigger the viewWillLoad etc. It's like it's waiting to any other bit of user input before it will progress. This happens on a real iPhone and in the simulator, which I proved by sending the shake command to the simulator.

I'm really at a loss as to what to do about this... I'd really appreciate any help anyone can provide.

Thanks

Here's my code. It's pretty simple...

This is code in questionViewController that acts as the delegate to the questionTableView

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
{
// If guess was wrong, then mark the selection as incorrect
NSLog(@"Incorrect Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
[cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];
}
else
{
// If guess was correct, show correct view
NSLog(@"Correct Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
self.correctViewController = [[HFBCorrectViewController alloc] init];
self.correctViewController.delegate = self;
[self presentViewController:self.correctViewController animated:YES completion:^(void){
NSLog(@"Completed Presenting correctViewController");
[self setUpViewForNextQuestion];
}];
}
}


This is the whole of the correctViewController

@implementation HFBCorrectViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
// Custom initialization
NSLog(@"[HFBCorrectViewController initWithNibName:bundle:]");
}
return self;
}

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
NSLog(@"[HFBCorrectViewController viewDidLoad]");
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"[HFBCorrectViewController viewDidAppear]");
}

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

- (IBAction)close:(id)sender
{
NSLog(@"[HFBCorrectViewController close:sender:]");
[self.delegate didDismissCorrectViewController];
}


@end


Edit:

I found this question earlier: UITableView and presentViewController takes 2 clicks to display

And if I change my
didSelectRow
code to this, it works very time with animation... But it's messy and doesn't make sense as to why it doesn't work in the first place. So I don't count that as an answer...

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
{
// If guess was wrong, then mark the selection as incorrect
NSLog(@"Incorrect Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
[cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];
// [cell setAccessoryType:(UITableViewCellAccessoryType)]

}
else
{
// If guess was correct, show correct view
NSLog(@"Correct Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);

////////////////////////////
// BELOW HERE ARE THE CHANGES
[self performSelector:@selector(showCorrectViewController:) withObject:nil afterDelay:0];
}
}

-(void)showCorrectViewController:(id)sender
{
self.correctViewController = [[HFBCorrectViewController alloc] init];
self.correctViewController.delegate = self;
self.correctViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:self.correctViewController animated:YES completion:^(void){
NSLog(@"Completed Presenting correctViewController");
[self setUpViewForNextQuestion];
}];
}

Answer

I've encountered the same issue today. I dug into the topic and it seems that it's related to the main runloop being asleep.

Actually it's a very subtle bug, because if you have the slightest feedback animation, timers, etc. in your code this issue won't surface because the runloop will be kept alive by these sources. I've found the issue by using a UITableViewCell which had its selectionStyle set to UITableViewCellSelectionStyleNone, so that no selection animation triggered the runloop after the row selection handler ran.

To fix it (until Apple does something) you can trigger the main runloop by several means:

The least intrusive solution is to call CFRunLoopWakeUp:

[self presentViewController:vc animated:YES completion:nil];
CFRunLoopWakeUp(CFRunLoopGetCurrent());

Or you can enqueue an empty block to the main queue:

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

It's funny, but if you shake the device, it'll also trigger the main loop (it has to process the motion events). Same thing with taps, but that's included in the original question :) Also, if the system updates the status bar (e.g. the clock updates, the WiFi signal strength changes etc.) that'll also wake up the main loop and present the view controller.

For anyone interested I wrote a minimal demonstration project of the issue to verify the runloop hypothesis: https://github.com/tzahola/present-bug

I've also reported the bug to Apple.