Can a presented view controller also be a presenting view controller? - ios

On top of an existing view I want to:
a) display a screen to the user
b) then send an SMS
c) display another screen to the user.
For a) I am doing this:
[[UIApplication sharedApplication].delegate.window.rootViewController presentViewController: firstController animated: NO completion:nil];
and for b) I am doing the same thing, except this is presenting a different vc of course, a MFMessageComposeViewController.
However in order for b) to appear I first have to dismiss the first view controller using:
[[UIApplication sharedApplication].delegate.window.rootViewController dismissViewControllerAnimated:NO completion: nil];
That works so far, I can see the first view appear then see the SMS compose view appear.
When the SMS is sent I am doing this to dismiss the SMS compose view
[[UIApplication sharedApplication].delegate.window.rootViewController dismissViewControllerAnimated:NO completion: nil];
But then nothing happens when I try to present another screen to the user using presentViewController. I can't see any reason why this should be, is there something I'm not aware of?
Actually the screen before the SMS view and after it are the same except they have different text, so the easiest sequence of steps would be:
a) present the view controller with text "abc"
b) present the SMS controller
c) when the SMS is sent dismiss the SMS controller
d) update the text in the first view controller using an IBOutlet
e) dismiss the first view controller.
However as mentioned earlier on, if I don't dismiss the first view controller the SMS controller will not appear. So my main question is how can I present the SMS controller on top of the first view controller?

You can either present one after the other closes:
UIViewController *rvc = [UIApplication sharedApplication].delegate.window.rootViewController;
[rvc dismissViewControllerAnimated:NO completion:^{
[rvc presentViewController: secondController animated: NO completion:nil];
}];
Or present another on top:
UIViewController *rvc = [UIApplication sharedApplication].delegate.window.rootViewController;
UIViewController *pvc = rvc.presentedViewController; // you may need to loop through presentedViewControllers if you have more than one
[pvc presentViewController: secondController animated: NO completion:nil];

I just tried it on iOS15. Yes a presented VC can present another VC.
So suppose you have:
VC1 --> present--> VC2
you can easily call present(VC3(), animated: true, completion: nil) on VC2 and things would work fine. You can happily end up with:
VC1 --> present--> VC2 --present--> VC3
FWIW when you dismiss VC3, it will only go back to VC2. It won't go back to VC1.

iOS does not allow you to open two modal views at the same time. The modal view is designed to be the topmost view.

In my case, I have access directly to the presented view controller so in that case:
self.present(viewControllerToPresent, animated: true) {
//It's presented.
}

extension UIViewController{
/// most top presented view controller
var presentedTop:UIViewController {
var ctrl:UIViewController = self;
while let presented = ctrl.presentedViewController {
ctrl = presented;
}
return ctrl;
}
}
// call somewhere someCtrl.presentedTop.present(vc, animated:true)

Related

How to dismiss pushed ViewController after logout?

I spent so many hours with this issue, so I can't find any solution.
I open SettingsViewController from my TopBar on tap in avatar with pushViewController method (here is problem) and from menu (not modal).
I want to dismiss this pushed ViewController when I tap on logout button.
Below is a function that I use in some VC's and works very well.
func goToSettingsView() {
let vc = SettingsViewController(nibName: "SettingsViewController", bundle: nil)
vc.modalPresentationStyle = .fullScreen
self.navigationController!.pushViewController(vc, animated: true)
for constraint in vc.view.constraints {
if constraint.identifier == "alignTopHeader" {
constraint.constant = 0
}
}
}
When I clicked logout button isn't working and when I login from (LoginViewController) this SettingsViewController is still showing, but I would go to Main Screen without any modals.
I did some ideas but not good working yet.
First idea was:
Below is my logout IBaction in my SettingsViewController:
- (void)logout {
[self dismissViewControllerAnimated:YES completion:nil];
[[UserManager shared] logoutUserWithSuccessBlock:^{
[self presentLoginViewController];
}];
}
LoginViewController is dismiss, but self is targeted for SettingsViewController?
Second idea:
I added a function declared in AppDelegate "backToRoot" in LoginViewController and call from viewWillDisappear.
[appDelegate backToRoot];
function in AppDelegate.m file
-(void)backToRoot {
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
PresentationsPickerViewController *mainvc = [[PresentationsPickerViewController alloc] init];
[self setCenterPanelWithViewController:mainvc];
}
But still not working with modal, it's work fine when SettingsViewController isn't modal.
Do you have any ideas how to hide/dismiss pushed SettingsViewController in logout action?
I want to dismiss this pushed ViewController when I tap on logout button.
Now you don't dismiss a pushed UIViewController. Push is a navigation style associated with a UINavigationController. When you push a UIViewController you need to pop it.
When we talk about dismiss, we usually mean closing a UIViewController that is presented modally. This method means that a new UIViewController is added on top of your previously shown UIViewController or any class inheriting from it. This does not get added onto the navigation stack. You can only dismiss a UIViewController if it was presented.
Now your first code block shows you pushing the SettingsViewController and your first solution tries to dismiss it. This will not work. You need to pop it from the UINavigationController to close it.
Next,
LoginViewController is dismiss, but self is targeted for SettingsViewController?
The method [self dismiss]... will close the screen that is the latest presented screen on top. If you present LoginViewController from settings, then LoginViewController screen gets dismissed.
Also,
But still not working with modal, it's work fine when SettingsViewController isn't modal. Do you have any ideas how to hide/dismiss pushed SettingsViewController in logout action?
If your SettingsViewController is presented then you need to dismiss it and if it is pushed, you need to pop it.
If there are situations when both the actions can occur, then on your close button action, you can check how the screen was displayed. Use this link to figure out the checks for that.
If all you want to do is go to your LoginViewController on logging out, you can just change the root window of your application.
let window = (UIApplication.shared.delegate as? AppDelegate)?.window
window?.rootViewController = viewController //this will be your loginViewController
window?.makeKeyAndVisible()
For iOS 13:
UIApplication.shared.windows.first?.rootViewController = LoginViewController
UIApplication.shared.windows.first?.makeKeyAndVisible()
It's really simple. If you want to dismiss a pushed UIViewController in a navigational-stack just pop it out, like this:
func logout() {
navigationController?.popViewController(animated: true)
}
You can simple dismiss all view controllers above the root view controller.
func logout() {
self.view.window!.rootViewController?.dismiss(animated: false, completion: nil)
}
Hope it will help. Thanks.

How to dismiss a viewcontroller within another ViewController?

Let say I have view controllers named A,B,C,D,E I open theseViewConrollersone after the other in presentModel way. When I am in ViewController E I want to open another ViewController F in presentModel way. In that F ViewController I have a back button. When I click on that It should dismiss the F and show the A ViewController. But now when it dismissse shows E. How can I dismiss all other viewcontrollers except the A when I click the back button of F
Please help me. Thanks
UPDATED
-(IBAction)dismisthis:(id)sender{
UIViewController *dismissingViewController = self.presentingViewController;
while (dismissingViewController.presentingViewController != nil && [dismissingViewController isKindOfClass:[FrontViewController class]]) {
dismissingViewController = self.presentingViewController;
}
[dismissingViewController dismissViewControllerAnimated:NO completion:NULL];
If you dismiss a view controller that is presenting another view controller, the entire hierarchy is dismissed. It doesn't matter how many levels there are. So all you have to do is find your view controller A and tell it to dismiss its presented view controller.
If A is always the bottom of the heap, you can use a simple loop to find it:
UIViewController *dismissingViewController = self.presentingViewController;
while (dismissingViewController.presentingViewController != nil) {
dismissingViewController = self.presentingViewController;
}
[dismissingViewController dismissViewControllerAnimated:YES
completion:NULL];
What about sending Notification using NSNotificationCenter from F and A will listen to it. Once A receives the notification, it will call dismissViewController which I think will dismiss all.
At this moment i can think of three solutions
You should keep track of all viewControllers in a stackObject(which is an array) in Appdelegate. When you want it access this get these array and dismiss all view controler objetcs.
Each viewcontroller you can observer for a NSNotification which will listen for notification. When you need it in "F" just post the notification and this notification will dismiss the eviewcontrollers
Go for NavigationController so that you can push to rootviewcontroller
Try this:
UIViewController *rootVC = [UIApplication sharedApplication].delegate.window.rootViewController;
[rootVC dismissViewControllerAnimated:YES completion:nil];
// One-liner
// [[UIApplication sharedApplication].delegate.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
Make A as rootViewController and use below code on Back button of "F":
[[[[UIApplication sharedApplication] keyWindow] rootViewController] dismissViewControllerAnimated:true completion:nil];

ViewController WILL NOT dismiss

WLINewPostViewController *newPostViewController = [[WLINewPostViewController alloc] initWithNibName:#"WLINewPostViewController" bundle:nil];
UINavigationController *newPostNavigationController = [[UINavigationController alloc] initWithRootViewController:newPostViewController];
newPostNavigationController.navigationBar.translucent = NO;
[tabBarController presentViewController:newPostNavigationController animated:YES completion:nil];
So I just simply push a new UIViewController.
Then after it posts the server callback calls a method with this code from the WLINewPostViewController.m:
[self dismissViewControllerAnimated:YES completion:^{
NSLog(#"Completed");
}];
[[self navigationController] popViewControllerAnimated:YES];
if (self == self.navigationController.visibleViewController){
NSLog(#"self = visibile");
}
if (self == self.presentingViewController.presentingViewController){
NSLog(#"self = presenting");
}
}
I tried a bunch of different things and none work.
I am relatively new to Xcode but after trying
[self dismissViewControllerAnimated:YES completion]
[self.navigationController popViewControllerAnimated:YES]
[self.navigationController.visibleViewController.presentedViewController dismissViewControllerAnimated:YES completion:nil];
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
and every other possibility, I am officially stumped. The WLINewPostViewController still won't dismiss.
It Logs out "self = visible"
Let me illustrate what you are trying to do
You have a navigation controller with Controller A.
Here you are trying to present another Controller B from Controller A.
Now when you get a callback from the server, you should call dismissViewControllerAnimated from Controller B to dismiss itself.
So after dismissViewControllerAnimated:completion: method call, the Controller B will be dismissed and Controller A will be shown automatically. Now you do not need to call popViewControllerAnimated: in completion block again as there is no other Controller in navigation controller to load.
If you have different use case, let me know I can provide solution.
You are presenting a view over navigationbar instead of pushing it over navigationbar.
When push you pop. When you present you dismiss. So instead of popViewControllerAnimated you need to use dismissViewControllerAnimated:completion
dismiss behaves differently depending on the receiver. From the docs:
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, it automatically forwards the message to the presenting view controller.
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. 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.
In short, if the vc on top calls it on itself, it dismisses itself. Anywhere else on the stack dismisses to that point, animating only the topmost vc.
What's extra confusing (for you and many others) is that the navigation vc has a stack too, and your problem is complicated further by presenting an navigation vc atop a tab-bar vc.
So what to do? The question is unclear about which vc is the receiver in the posted code (who is self in that snippet?). The text implies that self is a vc on the stack of the presented navigation vc, like...
TabBarVC --- presents ---> NavVC
| |
| --- viewControllers stack = rootVC, vc1
|
---> viewControllers for each tab
... and it's root or vc1 that wants to dismiss. If I'm right about that, then, given the docs, the solution is clear:
[self.navigationController dismissViewControllerAnimated:YES completion:^{}];
will put us back on the tabbar vc on whatever tab was visible when we did the present.

Dismiss Modal UIViewController during handoff

I'm trying to add handoff to my App. When my App is waken by Handoff I need to dismiss several ViewControllers to return to a MKMapView located in my view hierarchy.
My view hierarchy looks like:
+NavController
+--- MyMapViewController (contains the MKMapView where I want to go, top of the NavController)
+--- MyTabbarViewController (it's a tab bar with several views...)
+--- MyOverviewViewController (pushed on the stack of my NacController by a VC that displayed in MyTabbarViewController)
+--- MyDetailViewController (it's a modal view presented by MyOverviewController)
+--- MyChartViewController (it's a modal view presented by MyDetailViewController)
When the App receives the UserActivity the displayed view controller is "MyChartViewController".
To show the result of the User Activity I need to go to the MyMapViewController to display some information on the MapView.
My issue is to find a simple way to dismiss "MyChartViewController" and "MyDetailsViewController" before to call [navController popToRootViewControllerAnimated:TRUE];
I tried [navController.visibleViewController dismissViewControllerAnimated:FALSE completion:Nil]; but it dismisses only the MyChartViewController, why?
navController.visibleViewController is the MyDetailViewController.
The only solution I found is to call:
[MyChartViewController dismissViewControllerAnimated:FALSE completion:Nil];
AND
[navController.visibleViewController dismissViewControllerAnimated:FALSE completion:Nil];
My view hierarchy can be more complex or different depending on where the user is located in the App when the UserActivity is triggered. I'm expecting to find a simple solution that could be used for any situation. The main issue is to dismiss the stack of modals viewController.
I always want to go back to this MyMapViewController that is on the top of the NavController.
Any idea?
Thanks!
Please see below my simple solution. I have written a method that dismiss all ViewControllers displayed on the top of the visible view controller of my NavigationController.
When all "modal" viewControllers are dismissed I just pop the NavigationController to the root.
It seems it's working for all situations I have to manage in my App.
+(void) dismissModalAndPopToRoot:(UINavigationController*) navController {
if (navController.visibleViewController.parentViewController == Nil) {
// it's not a children of the NavController, so it's most probably a modal or several modals
UIViewController* currentViewController = navController.visibleViewController;
NSMutableArray* stackOfPresentedVC = [[NSMutableArray alloc] init];
while (currentViewController.presentedViewController != Nil) {
[stackOfPresentedVC addObject:currentViewController];
currentViewController = currentViewController.presentedViewController;
}
[stackOfPresentedVC addObject:currentViewController];
for (NSInteger i = (stackOfPresentedVC.count) - 1; i >= 0; i--) {
currentViewController = stackOfPresentedVC[i];
[currentViewController dismissViewControllerAnimated:FALSE completion:Nil];
}
}
[navController popToRootViewControllerAnimated:TRUE];
}
If the root view controller is always the MyMapViewController, then this task is kind of easy - just set the root view controller of the window again to be a UINavigationController that has the MyMapViewController as root.
I would set up and unwindSegue in the VC you want to unwind to, and then in the VC you want to dismiss I would check the conditions for the unWind in viewWillAppear: and call the unwind if it meets the conditions

Dismissing a ViewController lower in the stack does not behave as expected

I'm building a complex app that has kind of a branch in the middle.
At some point in the app, a particular UIViewController is presented, we'll call it mainViewController (shortened mainVC).
The mainVC presents another view controller, by code, using the following code (I strip out parts of it for privacy reasons):
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"SecondaryStoryboard" bundle:secondaryBundle];
SecondViewController *secondVC = [storyboard instantiateInitialViewController];
[self presentViewController:secondVC animated:YES completion:nil];
So the secondVC will later present another view controller, called thirdVC. This is done using a custom segue, set in the storyboard used in the code above, which code looks like this:
#implementation VCCustomPushSegue
- (void)perform {
UIView *sourceView = ((UIViewController *)self.sourceViewController).view;
UIView *destinationView = ((UIViewController *)self.destinationViewController).view;
UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
destinationView.center = CGPointMake(sourceView.center.x + sourceView.frame.size.width, destinationView.center.y);
[window insertSubview:destinationView aboveSubview:sourceView];
[UIView animateWithDuration:0.4
animations:^{
destinationView.center = CGPointMake(sourceView.center.x, destinationView.center.y);
sourceView.center = CGPointMake(0 - sourceView.center.x, destinationView.center.y);
}
completion:^(BOOL finished){
[self.sourceViewController presentViewController:self.destinationViewController animated:NO completion:nil];
}];
}
#end
As you can see this segue presents the destination view controller modally (by the use of presentViewController:) with a custom animation (a slide from right to left).
So basically up to here everything is fine. I present the secondVC with a classic modal animation (slide up from bottom) and present the thirdVC with my custom transition.
But when I want to dismiss the thirdVC, what I want is to go back directly to the mainVC. So I call the following from the thirdVC :
self.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:_animate completion:nil];
That way, I'm calling dismissViewControllerAnimated: directly on mainVC (referenced by self.presentingViewController.presentingViewController), and I'm expecting the thirdVC to be dismissed with an animation, and the secondVC to just disappear without animation.
As Apple says in the UIViewController Class Documentation:
The presenting view controller is responsible for dismissing the view
controller it presented. If you call this method on the presented view
controller itself, it automatically forwards the message to the
presenting view controller.
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.
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.
The issue is that it's not what happens. In my scenario, the thirdVC disappears, and shows the secondVC being dismissed with the classic modal slide to bottom animation.
What am I doing wrong ?
Edit :
So #codeFi's answer is probably working in a classic project, but the problem here is that I'm working on a framework. So mainVC would be in a client app, and the secondVC and thirdVC are in my framework, in a separate storyboard. I don't have access to mainVC in any other way than a reference to it in my code, so unwind segues are unfortunately not an option here.
I've been having this exact same issue, and I've managed to visually work around it by adding a snapshot of the screen as a subview to secondVC.view, like so:
if (self.presentedViewController.presentedViewController) {
[self.presentedViewController.view addSubview:[[UIScreen mainScreen] snapshotViewAfterScreenUpdates:NO]];
}
[self dismissViewControllerAnimated:YES completion:nil];
Not pretty, but it seems to be working.
NOTE: if your secondVC has a navigation bar, you will need to hide the navigation bar in between snapshotting the screen and adding the snapshot as a subview to secondVC, as otherwise the snapshot will appear below the navigation bar, thus seemingly displaying a double navigation bar during the dismissal animation. Code:
if (self.presentedViewController.presentedViewController) {
UIView *snapshot = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:NO];
[self.presentedViewController.navigationController setNavigationBarHidden:YES animated:NO];
[self.presentedViewController.view addSubview:snapshot];
}
[self dismissViewControllerAnimated:YES completion:nil];
I had the same issue and I've fixed it by using UnwindSegues.
Basically, all you have to do is add an IBAction Unwind Segue method in the ViewController that you want to segue to and then connect in IB the Exit action to your Unwind Segue method.
Example:
Let's say you have three ViewControllers (VC1, VC2, VC3) and you want to go from VC3 to VC1.
Step 1
Add a method to VC1 like the following:
- (IBAction)unwindToVC1:(UIStoryboardSegue*)sender
{
}
Step 2
Go in Interface Builder to VC3 and select it. Then CTRL-drag from your VC icon to Exit icon and select the method you've just added in VC1.
Step 3
While still in IB and with VC3 selected, select your Unwind Segue and in the Attributes Inspector add a Segue Identifier.
Step 4
Go to VC3 where you need to perform your segue (or dismiss the VC) and add the following:
[self performSegueWithIdentifier:#"VC1Segue" sender:self];

Resources