iPhone - dismiss multiple ViewControllers
I have a long View Controllers hierarchy;
in the first View Controller I use this code:
SecondViewController *svc = [[SecondVie开发者_开发知识库wController alloc] initWithNibName:@"SecondViewController" bundle:nil];
[self presentModalViewController:svc animated:YES];
[svc release];
In the second View Controller I use this code:
ThirdViewController *tvc = [[ThirdViewController alloc] initWithNibName:@"ThirdViewController" bundle:nil];
[self presentModalViewController:tvc animated:YES];
[tvc release];
and so on.
So there is a moment when I have many View Controllers and I need to come back to the first View Controller. If I come back one step at once, I use in every View Controller this code:
[self dismissModalViewControllerAnimated:YES];
If I want to go back directly from the, say, sixth View Controller to the first one, what I have to do to dismiss all the Controllers at once?
Thanks
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]]
.
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.
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)
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.
[[self presentingViewController]presentingViewController]dismissModalViewControllerAnimated:NO];
You can also implement a delegate in all controllers you want to dismiss
The problem with most solutions is that when you dismiss the stack of presented viewControllers, the user will briefly see the first presented viewController in the stack as it is being dismissed. Jakub's excellent solution solves that. Here is an extension based on his answer.
extension UIViewController {
func dismissAll(animated: Bool, completion: (() -> Void)? = nil) {
if let optionalWindow = UIApplication.shared.delegate?.window, let window = optionalWindow, let rootViewController = window.rootViewController, let presentedViewController = rootViewController.presentedViewController {
if let snapshotView = window.snapshotView(afterScreenUpdates: false) {
presentedViewController.view.addSubview(snapshotView)
presentedViewController.modalTransitionStyle = .coverVertical
}
if !isBeingDismissed {
rootViewController.dismiss(animated: animated, completion: completion)
}
}
}
}
Usage: Call this extension function from any presented viewController that you want to dismiss back to the root.
@IBAction func close() {
dismissAll(animated: true)
}
If you are using all are Model view controller you can use notification for dismissing all preseted view controller.
1.Register Notification in RootViewController like this
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(dismissModelViewController)
name:dismissModelViewController
object:nil];
2.Implement the dismissModelViewController function in rootviewController
- (void)dismissModelViewController
{
While (![self.navigationController.visibleViewController isMemberOfClass:[RootviewController class]])
{
[self.navigationController.visibleViewController dismissViewControllerAnimated:NO completion:nil];
}
}
3.Notification post every close or dismiss button event.
[[NSNotificationCenter defaultCenter] postNotificationName:dismissModelViewController object:self];
In Swift:
self.presentingViewController?.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
Try this..
ThirdViewController *tvc = [[ThirdViewController alloc] initWithNibName:@"ThirdViewController" bundle:nil];
[self.view addsubview:tvc];
[tvc release];
Swift 3 extension based upon the above answers.
Principle for a stack like that : A -> B -> C -> D
- Take a snapshot of D
- Add this snapshot on B
- Dismiss from B without animation
On completion, dismiss from A with animation
extension UIViewController { func dismissModalStack(animated: Bool, completion: (() -> Void)?) { let fullscreenSnapshot = UIApplication.shared.delegate?.window??.snapshotView(afterScreenUpdates: false) if !isBeingDismissed { var rootVc = presentingViewController while rootVc?.presentingViewController != nil { rootVc = rootVc?.presentingViewController } let secondToLastVc = rootVc?.presentedViewController if fullscreenSnapshot != nil { secondToLastVc?.view.addSubview(fullscreenSnapshot!) } secondToLastVc?.dismiss(animated: false, completion: { rootVc?.dismiss(animated: true, completion: completion) }) } } }
A little flickering on simulator but not on device.
First of all Oscar Peli thanks for your code.
To start your navigationController at the beginning, you could make it a little more dynamic this way. (in case you don't know the number of ViewControllers in stack)
NSArray *viewControllers = self.navigationController.viewControllers;
[self.navigationController popToViewController: [viewControllers objectAtIndex:0] animated: YES];
id vc = [self presentingViewController];
id lastVC = self;
while (vc != nil) {
id tmp = vc;
vc = [vc presentingViewController];
lastVC = tmp;
}
[lastVC dismissViewControllerAnimated:YES completion:^{
}];
Here is a solution that I use to pop and dismiss all view controllers in order to go back to the root view controller. I have those two methods in a category of UIViewController:
+ (UIViewController*)topmostViewController
{
UIViewController* vc = [[[UIApplication sharedApplication] keyWindow] rootViewController];
while(vc.presentedViewController) {
vc = vc.presentedViewController;
}
return vc;
}
+ (void)returnToRootViewController
{
UIViewController* vc = [UIViewController topmostViewController];
while (vc) {
if([vc isKindOfClass:[UINavigationController class]]) {
[(UINavigationController*)vc popToRootViewControllerAnimated:NO];
}
if(vc.presentingViewController) {
[vc dismissViewControllerAnimated:NO completion:^{}];
}
vc = vc.presentingViewController;
}
}
Then I just call
[UIViewController returnToRootViewController];
A swift version with some additions based on this comment
func dismissModalStack(viewController: UIViewController, animated: Bool, completionBlock: BasicBlock?) {
if viewController.presentingViewController != nil {
var vc = viewController.presentingViewController!
while (vc.presentingViewController != nil) {
vc = vc.presentingViewController!;
}
vc.dismissViewControllerAnimated(animated, completion: nil)
if let c = completionBlock {
c()
}
}
}
Simple recursive closer:
extension UIViewController {
final public func dismissEntireStackAndSelf(animate: Bool = true) {
// Always false on non-calling controller
presentedViewController?.ip_dismissEntireStackAndSelf(false)
self.dismissViewControllerAnimated(animate, completion: nil)
}
}
This will force close every child controller and then only animate self. You can toggle for whatever you like, but if you animate each controller they go one by one and it's slow.
Call
baseController.dismissEntireStackAndSelf()
Swift extension based upon the above answers:
extension UIViewController {
func dismissUntilAnimated<T: UIViewController>(animated: Bool, viewController: T.Type, completion: ((viewController: T) -> Void)?) {
var vc = presentingViewController!
while let new = vc.presentingViewController where !(new is T) {
vc = new
}
vc.dismissViewControllerAnimated(animated, completion: {
completion?(viewController: vc as! T)
})
}
}
Swift 3.0 version:
extension UIViewController {
/// Dismiss all modally presented view controllers until a specified view controller is reached. If no view controller is found, this function will do nothing.
/// - Parameter reached: The type of the view controller to dismiss until.
/// - Parameter flag: Pass `true` to animate the transition.
/// - Parameter completion: The block to execute after the view controller is dismissed. This block contains the instance of the `presentingViewController`. You may specify `nil` for this parameter.
func dismiss<T: UIViewController>(until reached: T.Type, animated flag: Bool, completion: ((T) -> Void)? = nil) {
guard let presenting = presentingViewController as? T else {
return presentingViewController?.dismiss(until: reached, animated: flag, completion: completion) ?? ()
}
presenting.dismiss(animated: flag) {
completion?(presenting)
}
}
}
Completely forgot why I made this as it is incredibly stupid logic considering most of the time a modal view controller's presenting view controller is UITabBarController
rendering this completely useless. It makes much more sense to actually acquire the base view controller instance and call dismiss
on that.
For Swift 3.0+
self.view.window!.rootViewController?.dismissViewControllerAnimated(false, completion: nil)
This will dismiss all presented view controllers on your rootviewcontroller.
Use this generic solution to solve this issue:
- (UIViewController*)topViewController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
- (void)dismissAllModalController{
__block UIViewController *topController = [self topViewController];
while (topController.presentingViewController) {
[topController dismissViewControllerAnimated:NO completion:^{
}];
topController = [self topViewController];
}
}
Dismiss the top VC animated and the other ones not. If you hace three modal VC
[self dismissModalViewControllerAnimated:NO]; // First
[self dismissModalViewControllerAnimated:NO]; // Second
[self dismissModalViewControllerAnimated:YES]; // Third
EDIT: if you want to do this only with one method, save you hierarchy into an array of VC and dismiss the last object animated and the other ones not.
In swift 4 And Xcode 9 This will helps you.
var vc : UIViewController = self.presentingViewController!
while ((vc.presentingViewController) != nil) {
vc = vc.presentingViewController!
}
vc.dismiss(animated: true, completion: nil)
Enjoy !!! :)
If you're going right back to the start, you can use the code [self.navigationController popToRootViewControllerAnimated:YES];
精彩评论