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]; }]; }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With