22

Is there a way to present a view controller modally without knowing what the visible view controller view is? Basically sort of like you would show an alert view at any points in time.

I would like to be able to do something like:

MyViewController *myVC = [[MyViewController alloc] init];
[myVC showModally];

I'd like to be able to call this from anywhere in the app, and have it appear on top. I don't want to care about what the current view controller is.

I plan to use this to show a login prompt. I don't want to use an alert view, and I also don't want to have login presentation code throughout the app.

Any thoughts on this? Or is there maybe a better way to achieve this? Should I just implement my own mechanism and just place a view on top of the window?

nebs
  • 4,939
  • 9
  • 41
  • 70

5 Answers5

31

Well, you can follow the chain.

Start at [UIApplication sharedApplication].delegate.window.rootViewController.

At each view controller perform the following series of test.

If [viewController isKindOfClass:[UINavigationController class]], then proceed to [(UINavigationController *)viewController topViewController].

If [viewController isKindOfClass:[UITabBarController class]], then proceed to [(UITabBarController *)viewController selectedViewController].

If [viewController presentedViewController], then proceed to [viewController presentedViewController].

Jeffery Thomas
  • 42,202
  • 8
  • 92
  • 117
  • 1
    I created a recursive method using your idea: https://gist.github.com/MartinMoizard/6537467. Works like a charm :) – MartinMoizard Sep 12 '13 at 13:47
  • This might not work sufficiently when there are one or more alert views open – at least for old style alerts, I found using `[UIApplication sharedApplication].keyWindow` as the more appropriate entry point for the chain. – DrMickeyLauer Apr 24 '17 at 10:37
  • @DrMickeyLauer yes, this is an old answer. There are things I would change. Now, I would advise iterating `var windows` in the shared application and iterating `var childViewControllers` in each view controller. – Jeffery Thomas Apr 25 '17 at 01:23
24

My solution in Swift (inspired by the gist of MartinMoizard)

extension UIViewController {
    func presentViewControllerFromVisibleViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
        if let navigationController = self as? UINavigationController {
            navigationController.topViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
        } else if let tabBarController = self as? UITabBarController {
            tabBarController.selectedViewController?.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
        } else if let presentedViewController = presentedViewController {
            presentedViewController.presentViewControllerFromVisibleViewController(viewControllerToPresent, animated: flag, completion: completion)
        } else {
            present(viewControllerToPresent, animated: flag, completion: completion)
        }
    }
}
Philippe Sabourin
  • 8,066
  • 3
  • 31
  • 46
allaire
  • 5,995
  • 3
  • 41
  • 56
15

This solution gives you the top most view controller so that you can handle any special conditions before presenting from it. For example, maybe you want to present your view controller only if the top most view controller isn't a specific view controller.

extension UIApplication {
    /// The top most view controller
    static var topMostViewController: UIViewController? {
        return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
    }
}

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else {
            return self
        }
    }
}

With this you can present your view controller from anywhere without needing to know what the top most view controller is

UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)

Or present your view controller only if the top most view controller isn't a specific view controller

if let topVC = UIApplication.topMostViewController, !(topVC is FullScreenAlertVC) {
    topVC.present(viewController, animated: true, completion: nil)
}

One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else if self is UIAlertController {
            return nil
        } else {
            return self
        }
    }
}
NSExceptional
  • 1,368
  • 15
  • 12
7

You could have this code implemented in your app delegate:

AppDelegate.m

-(void)presentViewControllerFromVisibleController:(UIViewController *)toPresent
{
    UIViewController *vc = self.window.rootViewController;
    [vc presentViewController:toPresent animated:YES];
}

AppDelegate.h

-(void)presentViewControllerFromVisibleViewController:(UIViewController *)toPresent;

From Wherever

#import "AppDelegate.h"
...
AppDelegate *delegate = [UIApplication sharedApplication].delegate;
[delegate presentViewControllerFromVisibleViewController:myViewControllerToPresent];

In your delegate, you're getting the rootViewController of the window. This will always be visible- it's the 'parent' controller of everything.

Undo
  • 25,519
  • 37
  • 106
  • 129
  • 7
    I think this will usually work, but it's not correct that the root view controller is always visible. There could be a modal view controller on screen, and I'm not sure what would happen if you ran this code under that circumstance. – rdelmar Apr 12 '13 at 01:33
  • @rdelmar In that case, your root VC will still be the root that is presenting the modal VC. – Undo Apr 12 '13 at 01:42
  • 4
    Yeah, it's still the root but it won't work -- if a modal view controller is on screen, and you try to present another one from the root vc, you get a warning, and the controller isn't presented (Attempt to present ... on whose view is not in the window hierarchy!). – rdelmar Apr 12 '13 at 03:08
  • @rdelmar Yes, I guess that's true. Moral of story: don't call modal VC's from modal VC's using this method. Use yourself as the... Wait - just **dont call modal VC's twice!** – Undo Apr 12 '13 at 03:52
  • It's perfectly fine to call modals twice, you just need to call the second one from the first one. – rdelmar Apr 12 '13 at 04:34
  • 1
    I ended up presenting the VC on the window's rootViewController but I accessed the window directly from UIApplication view keyWindow instead of going through the app delegate. Any particular reason you went through the app delegate here? – nebs Apr 13 '13 at 22:23
  • @Nebs Because that's the way I know how :) Didn't know about `keyWindow`. – Undo Apr 14 '13 at 01:05
2

I don't think you necessarily need to know which view controller is visible. You can get to the keyWindow of the application and add your modal view controller's view to the top of the list of views. Then you can make it work like the UIAlertView.

Interface file: MyModalViewController.h

#import <UIKit/UIKit.h>

@interface MyModalViewController : UIViewController
- (void) show;
@end

Implementation file: MyModalViewController.m

#import "MyModalViewController.h"


@implementation MyModalViewController

- (void) show {
    UIWindow *window = [[UIApplication sharedApplication] keyWindow];
    //  Configure the frame of your modal's view.
    [window addSubview: self.view];
}

@end
Jason Barker
  • 3,020
  • 1
  • 17
  • 11