presentViewController:animated:YES view will not appear until user taps again
IosUiviewcontrollerIos Problem Overview
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: https://stackoverflow.com/questions/20320591/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];
}];
}
Ios Solutions
Solution 1 - Ios
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.
Solution 2 - Ios
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:
Swift 4.2:
DispatchQueue.main.async {
self.present(myVC, animated: true, completion: nil)
}
Objective-C:
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:myVC animated:YES completion:nil];
});
Probably iOS7 is messing up the threads in didSelectRowAtIndexPath
.
Solution 3 - Ios
I bypassed it in Swift 3.0 by using the following code:
DispatchQueue.main.async {
self.present(UIViewController(), animated: true, completion: nil)
}
Solution 4 - Ios
Calling [viewController view]
on the view controller being presented did the trick for me.
Solution 5 - Ios
I'd be curious to see what [self setUpViewForNextQuestion];
does.
You could try calling [self.correctViewController.view setNeedsDisplay];
at the end of your completion block in presentViewController
.
Solution 6 - Ios
I've wrote extension (category) with method swizzling for UIViewController that solves the issue. Thanks to AXE and NSHipster for implementation hints (swift/objective-c).
Swift
extension UIViewController {
override public class func initialize() {
struct DispatchToken {
static var token: dispatch_once_t = 0
}
if self != UIViewController.self {
return
}
dispatch_once(&DispatchToken.token) {
let originalSelector = Selector("presentViewController:animated:completion:")
let swizzledSelector = Selector("wrappedPresentViewController:animated:completion:")
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
func wrappedPresentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
dispatch_async(dispatch_get_main_queue()) {
self.wrappedPresentViewController(viewControllerToPresent, animated: flag, completion: completion)
}
}
}
Objective-C
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(presentViewController:animated:completion:);
SEL swizzledSelector = @selector(wrappedPresentViewController:animated:completion:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)wrappedPresentViewController:(UIViewController *)viewControllerToPresent
animated:(BOOL)flag
completion:(void (^ __nullable)(void))completion {
dispatch_async(dispatch_get_main_queue(),^{
[self wrappedPresentViewController:viewControllerToPresent animated:flag completion:completion];
});
}
@end
Solution 7 - Ios
Check if your cell in the storyboard has Selection = none
If so, change it to blue or grey and it should work
Solution 8 - Ios
XCode Vesion : 9.4.1, Swift 4.1
In my case this happen, when I tap cell and move for the another view. I debug into the deeper and it seems that happen inside viewDidAppear
because of contains following code
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
then I added above code segment inside prepare(for segue: UIStoryboardSegue, sender: Any?)
and work perfect.
Within my experience, my solution is, If we'll hope to do any new changes(eg. table reload, deselect selected cell etc.) for the tableview when again come back from second view, then use delegate instead of viewDidAppear
and using above tableView.deselectRow
code segment before moving second view controller