Dismissing multiple modal view controllers at once?

10,542

Solution 1

Be sure that you're only calling dismissModalViewControllerAnimated: once.

I have found that asking to dismiss each stacked modal view controller will cause both of them to animate.

You have: A =modal> B =modal> C

You should only call [myViewControllerA dismissModalViewControllerAnimated:YES]

If you use [myViewControllerB dismissModalViewControllerAnimated:YES], it will dismiss C, and not B. In normal (unstacked) use, it would dismiss B (due to the responder chain bubbling the message up to A). In the stacked scenario that you describe B is a parent view controller and this takes precedence over being a modal view controller.

Solution 2

Although the accepted answer did work for me, it may be outdated now and left a weird looking animation where the topmost modal would immediately disappear and the animation would be on the rear modalview. I tried many things to avoid this and ended up having to use a bit of a hack to have it look nice. Note:(only tested in iOS8+, but should work iOS7+)

Basically, viewControllerA creates a UINavigationController with viewControllerB as the rootview and presents it modally.

// ViewControllerA.m
- (void)presentViewB {
    ViewControllerB *viewControllerB = [[ViewControllerB alloc] init];
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewControllerB];

    navigationController.modalPresentationStyle = UIModalPresentationFormSheet;
    [self presentViewController:navigationController animated:YES completion:nil];
}

Now in viewControllerB we are going to present viewControllerC the same way, but after we present it, we are going to put a snapshot of viewControllerC over the view layer on viewControllerB's navigation controller. Then, when viewControllerC disappears during dismissal, we won't see the change and the animation will look beautiful.

//ViewControllerB.m
- (void)presentViewC {
    ViewControllerC *viewControllerC = [[ViewControllerC alloc] init];

    // Custom presenter method to handle setting up dismiss and snapshotting 
    // I use this in a menu that can present many VC's so I centralized this part.
    [self presentViewControllerForModalDismissal:viewControllerC];
}

Below are my helper functions that are used to present the view and handle dismissal. One thing to note, I am using Purelayout for adding auto layout constraints. You can modify this to add them manually or get Purelayout at https://github.com/PureLayout/PureLayout

#pragma mark - Modal Presentation Helper functions
- (void)presentViewControllerForModalDismissal:(UIViewController*)viewControllerToPresent {
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewControllerToPresent];
    navigationController.modalPresentationStyle = UIModalPresentationFormSheet;

    // Ensure that anything we are trying to present with this method has a dismissBlock since I don't want to force everything to inherit from some base class. 
    NSAssert([viewControllerToPresent respondsToSelector:NSSelectorFromString(@"dismissBlock")], @"ViewControllers presented through this function must have a dismissBlock property of type (void)(^)()");
    [viewControllerToPresent setValue:[self getDismissalBlock] forKey:@"dismissBlock"];

    [self presentViewController:navigationController animated:YES completion:^{
        // We want the presented view and this modal menu to dismiss simultaneous. The animation looks weird and immediately becomes the menu again when dismissing.
        // So we are snapshotting the presented view and adding it as a subview so you won't see the menu again when dismissing.
        UIView *snapshot = [navigationController.view snapshotViewAfterScreenUpdates:NO];
        [self.navigationController.view addSubview:snapshot];
        [snapshot autoPinEdgesToSuperviewEdges];
    }];
}

- (void(^)()) getDismissalBlock {
    __weak __typeof(self) weakSelf = self;
    void(^dismissBlock)() = ^{
        __typeof(self) blockSafeSelf = weakSelf;
        [blockSafeSelf.navigationController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    };

    return dismissBlock;
}

Now we just need to ensure we have the dismissBlock defined as a property in ViewControllerC.h (you can obviously replace this whole part with delegate methods or other equally as exciting design patterns, the important part is to handle dismissal at the viewControllerB level)

// ViewControllerC.h
@interface ViewControllerC : UIViewController
@property (nonatomic, copy) void (^dismissBlock)(void);
@end

//ViewControllerC.m
// Make an method to handle dismissal that is called by button press or whatever logic makes sense.
- (void)closeButtonPressed {
    if (_dismissBlock)  {// If the dismissblock property was set, let the block handle dismissing
        _dismissBlock();
        return;
    }

    // Leaving this here simply allows the viewController to be presented modally as the base as well or allow the presenter to handle it with a block.
    [self dismissViewControllerAnimated:YES completion:nil];
}

Hope this helps, happy programming :)

Solution 3

For anyone looking for a work around you can do this:

  1. Cover everything with a snapshot of the window.
  2. Dismiss both view controllers without animation.
  3. Present a copy of the snapshot in another view controller without animation.
  4. Remove the snapshot covering the window.
  5. Dismiss the snapshot view controller with animation.

Here's the code:

let window = UIApplication.shared.keyWindow!
let snapshot = window.snapshotView(afterScreenUpdates: false)!
window.addSubview(snapshot)

let baseViewController = self.presentingViewController!.presentingViewController!

baseViewController.dismiss(animated: false) {
    let snapshotCopy = snapshot.snapshotView(afterScreenUpdates: false)!
    let snapshotViewController = UIViewController()
    snapshotViewController.view.addSubview(snapshotCopy)

    baseViewController.present(snapshotViewController, animated: false) {
        snapshot.removeFromSuperview()
        baseViewController.dismiss(animated: true, completion: nil)
    }
}

Solution 4

Here's a simple way in you can "dismiss to home":

    var vc: UIViewController = self
    while vc.presentingViewController != nil {
        vc = vc.presentingViewController!
    }
    vc.dismiss(animated: true, completion: nil)
Share:
10,542

Related videos on Youtube

sebrock
Author by

sebrock

Updated on May 25, 2021

Comments

  • sebrock
    sebrock almost 3 years

    So have a stack with three view controllers where A is root, B is first modal view controller and C is third modal vc. I would like to go from C to A at once. I have tried this solution to dismiss.It does work but not in a correct way. That is when the last view controller is dismissed it will breifly show the second view controller before the first is shown. What I'm looking for is a way to get from the third vc to the first in one nice animation without noticing the second view. Any help on this is greatly appriciated.

  • ohhorob
    ohhorob almost 14 years
    That's a great solution if A is the root of a UINavigationController
  • lucius
    lucius almost 14 years
    Oh yeah. I just woke up, so I didn't catch that.
  • sebrock
    sebrock almost 14 years
    Well as of now I use:[[[self parentViewController] parentViewController] dismissModalViewControllerAnimated:YES]; That would use the root viewcontroller. However, it still shows the second vc for a breif second.
  • sebrock
    sebrock almost 14 years
    Oh, and I should say that I use the Utility template and the second vc is the flipside view. The third is a modal vc instantiated from the flipside.
  • Tom Kidd
    Tom Kidd almost 12 years
    NOTE: in iOS5 this changed to "presentingViewController" game4mob.com/index.php/jawbreaker/…