iPhone - dismiss multiple ViewControllers

Solution 1:

Yes. there are already a bunch of answers, but I'm just going to add one to the end of the list anyway. The problem is that we need to get a reference to the view controller at the base of the hierarchy. As in @Juan Munhoes Junior's answer, you can walk the hierarchy, but there may be different routes the user could take, so that's a pretty fragile answer. It is not hard to extend this simple solution, though to simply walk the hierarchy looking for the bottom of the stack. Calling dismiss on the bottom one will get all the others, too.

-(void)dismissModalStack {
    UIViewController *vc = self.presentingViewController;
    while (vc.presentingViewController) {
        vc = vc.presentingViewController;
    }
    [vc dismissViewControllerAnimated:YES completion:NULL];
}

This is simple and flexible: if you want to look for a particular kind of view controller in the stack, you could add logic based on [vc isKindOfClass:[DesiredViewControllerClass class]].

Solution 2:

I found the solution.

Of course you can find the solution in the most obvious place so reading from the UIViewController reference for the dismissModalViewControllerAnimated method ...

If you present several modal view controllers in succession, and thus build a stack of modal view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack.

so it's enough to call the dismissModalViewControllerAnimated on the target View. I used the following code:

[[[[[self parentViewController] parentViewController] parentViewController] parentViewController] dismissModalViewControllerAnimated:YES];

to go back to my home.

Solution 3:

iOS 8+ universal method for fullscreen dismissal without wrong animation context. In Objective-C and Swift

Objective-C

- (void)dismissModalStackAnimated:(bool)animated completion:(void (^)(void))completion {
    UIView *fullscreenSnapshot = [[UIApplication sharedApplication].delegate.window snapshotViewAfterScreenUpdates:false];
    [self.presentedViewController.view insertSubview:fullscreenSnapshot atIndex:NSIntegerMax];
    [self dismissViewControllerAnimated:animated completion:completion];
}

Swift

func dismissModalStack(animated: Bool, completion: (() -> Void)?) {
    if let fullscreenSnapshot = UIApplication.shared.delegate?.window??.snapshotView(afterScreenUpdates: false) {
        presentedViewController?.view.addSubview(fullscreenSnapshot)
    }
    if !isBeingDismissed {
        dismiss(animated: animated, completion: completion)
    }
}

tl;dr

What is wrong with other solutions?

There are many solutions but none of them count with wrong dismissing context so:

e.g. root A -> Presents B -> Presents C and you want to dismiss to the A from C, you can officialy by calling dismissViewControllerAnimated on rootViewController.

 [[UIApplication sharedApplication].delegate.window.rootViewController dismissModalStackAnimated:true completion:nil];

However call dismiss on this root from C will lead to right behavior with wrong transition (B to A would have been seen instead of C to A).


so

I created universal dismiss method. This method will take current fullscreen snapshot and place it over the receiver's presented view controller and then dismiss it all. (Example: Called default dismiss from C, but B is really seen as dismissing)

Solution 4:

Say your first view controller is also the Root / Initial View Controller (the one you nominated in your Storyboard as the Initial View Controller). You can set it up to listen to requests to dismiss all its presented view controllers:

in FirstViewController:

- (void)viewDidLoad {
    [super viewDidLoad];

    // listen to any requests to dismiss all stacked view controllers
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dismissAllViewControllers:) name:@"YourDismissAllViewControllersIdentifier" object:nil];

    // the remainder of viewDidLoad ...
}

// this method gets called whenever a notification is posted to dismiss all view controllers
- (void)dismissAllViewControllers:(NSNotification *)notification {
    // dismiss all view controllers in the navigation stack
    [self dismissViewControllerAnimated:YES completion:^{}];
}

And in any other view controller down the navigation stack that decides we should return to the top of the navigation stack:

[[NSNotificationCenter defaultCenter] postNotificationName:@"YourDismissAllViewControllersIdentifier" object:self];

This should dismiss all modally presented view controllers with an animation, leaving only the root view controller. This also works if your initial view controller is a UINavigationController and the first view controller is set as its root view controller.

Bonus tip: It's important that the notification name is identical. Probably a good idea to define this notification name somewhere in the app as a variable, as not to get miscommunication due to typing errors.