While working on an additional section for my book, I encountered a tricky situation. Pause for a moment and ponder how you would solve this: You present a modal view controller. Some action within it communicates via delegate to the presenting view controller and this dismisses it. You need to present a new modal view controller right after the first one has animated out.
You cannot present a new view controller while a previous presentation animation is in progress. If you try, you get an error message like this:
Warning: Attempt to present <UINavigationController: 0x15fe3df70> on <MediaListViewController: 0x15fd0dbe0> while a presentation is in progress!
In the examples below I am using this helper method for getting a view controller from the storyboard and presenting it:
- (void)_presentModalVC { // load VC from story board so that we get the button wired up ModalViewController *newVC = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"modalVC"];; newVC.delegate = self; [self presentViewController:newVC animated:YES completion:NULL]; } |
Triggering on a Fuse
One way to avoid this warning – which is also an error since nothing is being presented – is to execute the follow-up presentation after a certain delay.
[self dismissModalViewControllerAnimated:YES]; [self performSelector:@selector(_presentModalVC) withObject:nil afterDelay:0.51]; |
If you are well versed in GCD, the following does the same, but looks way more sophisticated.
[self dismissModalViewControllerAnimated:YES]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.51 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self _presentModalVC]; }); |
Both are equally bad, especially now that Apple has changed the dismissal animation duration. Before iOS 8 this was around 0.33 seconds, now it is about half a second. Clearly, basing your logic on guesstimated Apple animation timings cannot be a good idea.
Triggering in viewDidAppear:
A workaround for the duration-guessing dilemma would be to rely on iOS calling viewWillAppear: before the dismissal animation and viewDidAppear: afterwards. One slight problem comes from the fact that those are called both on the initial showing of the view controller as well as when returning from a modal VC.
This means you’d need some form to tell apart whether the VC is appearing for the first time or RE-appearing. Let’s have an IVAR! The following example uses a named unwind segue to unwind the modal presentation.
@implementation ViewController { BOOL _showModalVCAfterViewDidAppear; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear: animated]; if (_showModalVCAfterViewDidAppear) { _showModalVCAfterViewDidAppear = NO; [self _presentModalVC]; } } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { ModalViewController *mvc = segue.destinationViewController; mvc.delegate = self; } - (IBAction)unwindFromModal:(UIStoryboardSegue *)segue { // intentionally left blank } - (void)modalViewControllerDidFinishSomething:(ModalViewController *)modalViewController { _showModalVCAfterViewDidAppear = YES; [modalViewController performSegueWithIdentifier:@"unwindSegue" sender:self]; } |
Because the performSegueWithIdentifier:sender: method lacks a completion handler the above is a workable workaround.
“Wait a minute!” you might say, “I can check the animated parameter!. This is YES coming back from the modal VC and NO for the root VC!”.
This was true all the way through iOS 7. Unfortunately this method breaks down because Apple changed the semantics there. At the time of this writing, the animated property remains NO even returning from the modal presentation under iOS 8.
So, an IVAR it is…
Proper Completion Handling
In iOS 5, Apple revamped the handling of modal view controller adding the more versatile methods presentViewControllerAnimated:completion: and dismissViewControllerAnimated:completion:. Those replaced presentModalViewControllerAnimated: and dismissModalViewControllerAnimated: which got fully deprecated in iOS 6. Code still using the deprecated methods causes this warning:
You might find legacy code where somebody only replaced the old methods with the new (to quiet the warning), but left the time-delay intact. For about 3 years now, the proper way to achieve the follow-up presentation has been:
- (void)modalViewControllerDidFinishSomething:(ModalViewController *)modalViewController { [self dismissViewControllerAnimated:YES completion:^{ [self _presentModalVC]; }]; } |
The completion block you pass here gets executed after the end of the animation transaction governing the view controller dismissal. This approach does not need to care about how long animations are taking, the contract implied by this API is: “we’ll execute this when it’s done”.
Unwinding Completion Revisited
You have seen above that there a disadvantage in programmatic unwinding because there was no completion handler. Beginning with iOS 7, Apple is using a transition coordinator object for all transitions. Both involved view controllers get a reference to a single transition coordinator which you can use to animate alongside the transition’s animations as well as specify a completion handler.
- (void)modalViewControllerDidFinishSomething:(ModalViewController *)modalViewController { [modalViewController performSegueWithIdentifier:@"unwindSegue" sender:self]; [self.transitionCoordinator animateAlongsideTransition:NULL completion:^(id context) { [self _presentModalVC]; }]; } |
This executes the named unwind segue owned by the modal VC.Right after the performSegue call the transition coordinator is set and so we can use its completion handler for our purpose for re-showing the modalVC. It makes no difference in the above sample whether you get the transitionCoordinator from the source or destination VC. Same instance.
Beware! There is a gaping functionality hole here. Grabbing the transition coordinator only works when triggering the unwind segue programmatically. In all the other places where you could get a reference to the involved view controllers there is no transition coordinator set (yet). Not even in the unwinding IBAction.
This means that there is no elegant way to handle a transition completion if you’re unwinding a presentation from a button or other UI element. Did anybody mention “IVAR + viewDidAppear”?
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear: animated]; if (_showModalVCAfterViewDidAppear) { _showModalVCAfterViewDidAppear = NO; [self _presentModalVC]; } } - (IBAction)unwindFromModal:(UIStoryboardSegue *)segue { _showModalVCAfterViewDidAppear = YES; } |
Oh well… I filed a Radar (enhancement) as we are used to doing for gaping functionality holes like this one.
Conclusion
Of all the methods you would employ to execute code following a modal view controller’s dismissal, the worst possible method is to estimate an animation duration. Where possible you should use the obvious completion handler, or where you can use the one provided by the transition coordinator.
Storyboard-driven unwinds are one remaining scenario where you have to put your follow-up code into viewDidAppear. But be weary of checking the animation BOOL parameter since this looks like it is changing semantics in iOS 8.
Categories: Recipes
And if you want a “flicker free” transition (i.e., you show a modal over your root, and when it dismisses you want a second modal to appear but without letting the user see a flicker of the root view), that is now different in iOS 8 too, in iOS 7 presenting it in the completion handler of the dismiss worked perfectly, but in iOS 8 it doesn’t.