38
votes

I'm dismissing a modal view controller and then immediately presenting another one, but the latter never happens. Here's the code:

 [self dismissModalViewControllerAnimated:YES];

 UIImagePickerController *picker = [[UIImagePickerController alloc] init];
 picker.delegate = self;
 picker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
 [self presentModalViewController:picker animated:YES];

The first modal VC slides down, but the new picker never comes up. Any idea as to what's going on?

8
It's not good idea to create new instance of UIImagePickerController, can you use the old one ?h4cky
can you expand on this? why would it be a bad idea?Heavy_Bullets

8 Answers

16
votes

Like other animated things, dismissModalViewControllerAnimated doesn't block until the view controller disappears. Instead it "kicks off" dismissal of the view controller. You might need to use a callback in viewDidDisappear of modal controller 1 that calls something like modalViewControllerDisappeared in the parent view controller. In that method you present modal controller 2. Otherwise what Robot K said.

52
votes

Aug 2012 Update:

iOS 5 and greater have introduced safer APIs for doing things after modals have animated into / out of place using completion blocks:

[self presentViewController:myModalVC animated:YES completion:^{}];
[self dismissViewControllerAnimated:YES completion:^{}];

Pre-Aug 2012 Answer:

I encountered a similar problem when dismissing modal one and then presenting modal two in rapid succession. Sometimes modal two would show after the modal one was dismissed and sometimes modal two wouldn't appear at all and that made me very sad.

Looks like a race condition to me...

Putting a 1+ second delay on the caller of the method that presented modal two, showModalTwo, made modal two appear every time after modal one was dismissed:

- (void)didDismissModalOne {
    [self performSelector:@selector(showModalTwo:) 
               withObject:someNumber 
               afterDelay:1.0f];
}

This confirmed a suspicion that there was some sort of race condition between the dismissal of modal one and the presentation of modal two. Putting a delay on the caller, however, is inelegant and did not guarantee that the race condition wouldn't re-appear under other circumstances.

The problem

Turns out that UIViewControllers have a public property, modalViewController, that gets set up when presentModalViewController:animated: is called and torn down when dismissModalViewControllerAnimated: is called. The catch is that it doesn't get torn down synchronously, so it's possible to create a race between removing the old value of modalViewController and setting up a new value in the following way.

  1. Present modal one. myViewController.modalViewController now points to modal one
  2. Dismiss modal one. Background process to tear down myViewController.modalViewController has started, but myViewController.modalViewController the still points to modal one
  3. Present modal two, myViewController.modalViewController] now points to modal two
  4. System callback fires, setting myViewController.modalViewController to nil, this interrupts the process of modal two animating in and the result is the user never sees it.

The race starts on step 2 and manifests on step 4.

The solution

My solution was to put a guard condition on the method that presented modal two to ensure that myViewControoler.modalViewController was nil before attempting to present modal two.

-(void)showModalTwo:(NSNumber *)aParameter {

    if (self.modalViewController) {        
            [self performSelector:@selector(showModalTwo:)
                       withObject:aParameter 
                       afterDelay:0.1f];
            return;
    }
    // You can now present the second modal safely.
}

Worked like a charm. A more elegant solution might include a timeout.

Post script

I really didn't like the polling aspect of this solution. @Nimrod suggests, in the accepted answer to this question, that you can safely initiate the presentation of modal two from the viewDidDisappear: method of modal one. I liked the sound of this event driven approach, but after doing a full implementation in my use case I confirmed that the race condition persisted when presenting modal two using a callback inside viewDidDisappear:. The only way to be absolutely sure that modal two will be presented is to poll inside the parent view controller until you're absolutely sure that self.modalViewController is nil. Then and only then is it "safe" to pop modal two.

15
votes
[self dismissViewControllerAnimated:YES completion:^{
    //Present the new MVC 

}];

Note: Available iOS 5.0 onwards.

5
votes
[self dismissModalViewControllerAnimated:NO];

 UIImagePickerController *picker = [[UIImagePickerController alloc] init];
 picker.delegate = self;
 picker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
 [self presentModalViewController:picker animated:YES];
2
votes

What's happening is that the view controller removes it's reference to the modal view controller once the dismiss animation is done, which happens after this code is called, so it doesn't think it has a new view controller to present modally.

How I've dealt with this is to set a didDismissModalVC ivar to YES after I call dismissModalViewController. Then in my viewDidAppear: method, I check the value of the ivar and present then new modal view controller. (Remembering to also set the value back to NO so I don't get stuck eternally dismissing modal view controllers.)

0
votes

In this case, I create delegate to callback parent view controller to show the second modal view controller.

Definition protocol of the parent view controller:

@protocol ParentViewControllerDelegate
- (void)showModalTwo;
@end

I implement this protocol in the parent view controller to show the second modal view controller and create the delegate property @property id<ParentViewControllerDelegate> delegate; on the first modal view controller.

Show the first modal view controller from parent view controller:

TheFirstModalViewController *controller = ...
controller.delegate = self;
[self presentViewController:controller animated:YES completion:nil];
...

On viewDidDisappear: method of the first modal view controller, just call delegate.showModalTwo: to show the second modal view from parent view controller.

Hope this help.

0
votes

In Swift:

  1. Use dismissViewController to dismiss the 1st presented view.
  2. Use dismissViewController's completion block to call a function in the mainVC. That function should call the second VC.

Your dismissViewController should look like this:

var presentingVC_Delegate: mainLists_PopoverDelegation!

@IBAction fund button_Pressed (sender: AnyObject) {
    self.dismissViewControllerAnimated(true, completion: { finished in
        self.presentingVC_Delegate.presentOtherVC()
        print("DismissVC completion block says hello")
    })
}

Where the mainVC houses that presentOtherVC:

func presentSettingsVC () {
    self.performSegueWithIdentifier("present_OtherVC", sender: nil)
}
0
votes

Here's my approach that seems to work nicely on iOS 10. My circumstance is slightly different, but should work for most situations. I'm presenting the initial viewController as a popover which requires a modal viewController to be presented immediately.

First, in the initial viewController's viewDidLoad simply hide it's view:

   view.isHidden = true

Then, on it's viewWillAppear, present the modal viewController, unanimated and un-hide the view on completion:

   present(yourModalViewController, animated: false) { [unowned self]
       self.view.isHidden = false
    }

You'll probably want to control your state with a Bool so that subsequent calls to viewWillAppear don't re-present the modal, but you get the idea.