Reputation: 861
I have an app that has an initial login screen then when the user wants to sign up, they are presented with a registration form that is three view controllers presented modally. When the user completes the form on the third screen (by pressing a "Done" button), I want the user to be taken back to the initial login screen.
I have tried doing this in the third view controller:
[self dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil]
However it only dismissed two of the view controllers and not all 3. Why did this happen?
Upvotes: 3
Views: 1766
Reputation: 32802
As other people pointed out, there are more elegant/efficient/easier ways to achieve similar results from the UX perspective: via a navigation controller, or a page view controller, or other container.
Short/quick answer: you need to go one step further in the chain of presenting view controllers, because the dismissal request needs to be sent to the controller that's presenting, and not to the one that's being presented. And you can send the dismiss request to that controller only, it will take care of popping from the stack the child controllers.
UIViewController *ctrl = self.presentingViewController.presentingViewController.presentingViewController;
[ctrl dismissViewControllerAnimated:NO completion:nil]
To explain why, and hopefully help other people better understand the controller presenting logic in iOS, below you can find are more details.
Let's start from Apple documentation on dismissViewControllerAnimated:completion:
Dismisses the view controller that was presented modally by the view controller.
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
Thus [self dismissViewControllerAnimated:NO completion:nil]
simply forwarded the request to self.presentingViewController
. Which means the first two lines had the same effect (actually the 2nd line did nothing as there was no presented controller after the 1st one executed).
This is why your dismissal of view controllers worked only the top 2 ones. You should've start with self.presentingViewController
and go along the chain of presenting view controllers. But this is not very elegant and can cause problems if later on the hierarchy of view controllers changes.
Continuing to read on the documentation, we stumble upon this:
If you present several view controllers in succession, thus building a stack of presented 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.
So you needn't call dismissViewControllerAnimated:completion:
three times, a call on the controller that you want to come back will suffice. At this point, passing a reference to that controller would be more reliable than navigating through the stack of view controllers.
There are some more useful details in the documentation, for example regarding what transitions apply when dismissing multiple controllers at once.
I recommend you go through the whole documentation, not only for this method, but for all methods/classes that you use in your application. You'll likely discover things that will make your life easier.
And if you don't have the time to read all Apple's documentation on UIKit, you can read it when you run into problems, like in this case with dismissViewControllerAnimated:completion:
not working as you thought it would.
As a closing note, there are some more subtle issues with your approach, as the actual dismissal takes place in another runloop cycle, as it's possible to generate console warnings and not behave as expected. This is why further actions regarding presenting/dismissing other controllers should be done in the completion block, to give a change to UIKit
to finish updating its internal state.
Upvotes: 2
Reputation: 1466
I know three ways to dismiss several viewControllers:
~
UIViewController *theVC = self.presentingViewController;
UIViewController *theOtherVC = theVC.presentingViewController;
[self dismissViewControllerAnimated:NO
completion:^
{
[theVC dismissViewControllerAnimated:NO
completion:^
{
[theOtherVC dismissViewControllerAnimated:NO completion:nil];
}];
}];
~
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.shouldDismiss)
{
CustomVC *theVC = (id)self.presentingViewController;
theVC.shouldDismiss = YES;
[self dismissViewControllerAnimated:NO completion:nil];
}
}
(This is the best approach so far)
Imagine you have some StandardVC, which presented LoginVC1.
Then, LoginVC1 presented LoginVC2.
Then, LoginVC2 presented LoginVC3.
An easy way of doing what you want would be to call (from inside your LoginVC3.m file)
[myLoginVC1 dismissViewControllerAnimated:YES completion:nil];
In this case your LoginVC1 would lose its strong reference (from StandardVC), which means that both LoginVC2 and LoginVC3 would also be deallocated.
So, all you need to do is let your LoginVC3 know that LoginVC1 exists.
If you don't want to pass a reference of LoginVC1, you can use:
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil];
However, the above approaches are NOT the correct ways of doing what you want to do.
I would recommend you doing the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (!self.isUserLoggedIn)
{
[UIApplication sharedApplication].keyWindow.rootViewController = self.myLoginVC;
}
return YES;
}
Then, when user finished his login process, you can use
[UIApplication sharedApplication].keyWindow.rootViewController = self.myUsualStartVC;
Upvotes: 0
Reputation: 4016
Totally understood. What I will do is embed a navigation controller instead of using modal. I have a case just like you. I have LoginViewController
to be the root view controller of the UINavigationController
. SignupViewController
will be presented by push
method. For ResetPasswordViewController
, I will use modal
because it's supposed to go back to LoginViewController
no matter the results. Then, you can dismiss the whole UINavigationController
from SignupViewController
or LoginViewController
.
Second approach will be like, you come up with your own mechanism to reference the presented UIViewController
via a shared instance. Then, you can easily dismiss it. Be careful with the memory management. After dismissing it, you should consider whether you need to nil it right away.
Upvotes: 0