Get the current displaying UIViewController on the screen in AppDelegate.m

194,044

Solution 1

You can use the rootViewController also when your controller is not a UINavigationController:

UIViewController *vc = self.window.rootViewController;

Once you know the root view controller, then it depends on how you have built your UI, but you can possibly find out a way to navigate through the controllers hierarchy.

If you give some more details about the way you defined your app, then I might give some more hint.

EDIT:

If you want the topmost view (not view controller), you could check

[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];

although this view might be invisible or even covered by some of its subviews...

again, it depends on your UI, but this might help...

Solution 2

I always love solutions that involve categories as they are bolt on and can be easily reused.

So I created a category on UIWindow. You can now call visibleViewController on UIWindow and this will get you the visible view controller by searching down the controller hierarchy. This works if you are using navigation and/or tab bar controller. If you have another type of controller to suggest please let me know and I can add it.

UIWindow+PazLabs.h (header file)

#import <UIKit/UIKit.h>

@interface UIWindow (PazLabs)

- (UIViewController *) visibleViewController;

@end

UIWindow+PazLabs.m (implementation file)

#import "UIWindow+PazLabs.h"

@implementation UIWindow (PazLabs)

- (UIViewController *)visibleViewController {
    UIViewController *rootViewController = self.rootViewController;
    return [UIWindow getVisibleViewControllerFrom:rootViewController];
}

+ (UIViewController *) getVisibleViewControllerFrom:(UIViewController *) vc {
    if ([vc isKindOfClass:[UINavigationController class]]) {
        return [UIWindow getVisibleViewControllerFrom:[((UINavigationController *) vc) visibleViewController]];
    } else if ([vc isKindOfClass:[UITabBarController class]]) {
        return [UIWindow getVisibleViewControllerFrom:[((UITabBarController *) vc) selectedViewController]];
    } else {
        if (vc.presentedViewController) {
            return [UIWindow getVisibleViewControllerFrom:vc.presentedViewController];
        } else {
            return vc;
        }
    }
}

@end

Swift Version

public extension UIWindow {
    public var visibleViewController: UIViewController? {
        return UIWindow.getVisibleViewControllerFrom(self.rootViewController)
    }

    public static func getVisibleViewControllerFrom(_ vc: UIViewController?) -> UIViewController? {
        if let nc = vc as? UINavigationController {
            return UIWindow.getVisibleViewControllerFrom(nc.visibleViewController)
        } else if let tc = vc as? UITabBarController {
            return UIWindow.getVisibleViewControllerFrom(tc.selectedViewController)
        } else {
            if let pvc = vc?.presentedViewController {
                return UIWindow.getVisibleViewControllerFrom(pvc)
            } else {
                return vc
            }
        }
    }
}

Solution 3

Simple extension for UIApplication in Swift (cares even about moreNavigationController within UITabBarController on iPhone):

extension UIApplication {
    class func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {

        if let nav = base as? UINavigationController {
            return topViewController(base: nav.visibleViewController)
        }

        if let tab = base as? UITabBarController {
            let moreNavigationController = tab.moreNavigationController

            if let top = moreNavigationController.topViewController where top.view.window != nil {
                return topViewController(top)
            } else if let selected = tab.selectedViewController {
                return topViewController(selected)
            }
        }

        if let presented = base?.presentedViewController {
            return topViewController(base: presented)
        }

        return base
    }
}

Simple usage:

    if let rootViewController = UIApplication.topViewController() {
        //do sth with root view controller
    }

Works perfect:-)

UPDATE for clean code:

extension UIViewController {
    var top: UIViewController? {
        if let controller = self as? UINavigationController {
            return controller.topViewController?.top
        }
        if let controller = self as? UISplitViewController {
            return controller.viewControllers.last?.top
        }
        if let controller = self as? UITabBarController {
            return controller.selectedViewController?.top
        }
        if let controller = presentedViewController {
            return controller.top
        }
        return self
    }
}

Solution 4

You could also post a notification via NSNotificationCenter. This let's you deal with a number of situations where traversing the view controller hierarchy might be tricky - for example when modals are being presented, etc.

E.g.,

// MyAppDelegate.h
NSString * const UIApplicationDidReceiveRemoteNotification;

// MyAppDelegate.m
NSString * const UIApplicationDidReceiveRemoteNotification = @"UIApplicationDidReceiveRemoteNotification";

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

    [[NSNotificationCenter defaultCenter]
     postNotificationName:UIApplicationDidReceiveRemoteNotification
     object:self
     userInfo:userInfo];
}

In each of your View Controllers:

-(void)viewDidLoad {
    [[NSNotificationCenter defaultCenter] 
      addObserver:self
      selector:@selector(didReceiveRemoteNotification:)                                                  
      name:UIApplicationDidReceiveRemoteNotification
      object:nil];
}

-(void)viewDidUnload {
    [[NSNotificationCenter defaultCenter] 
      removeObserver:self
      name:UIApplicationDidReceiveRemoteNotification
      object:nil];
}

-(void)didReceiveRemoteNotification:(NSDictionary *)userInfo {
    // see http://stackoverflow.com/a/2777460/305149
   if (self.isViewLoaded && self.view.window) {
      // handle the notification
   }
}

You could also use this approach to instrument controls which need to update when a notification is received and are used by several view controllers. In that case, handle the add/remove observer calls in the init and dealloc methods, respectively.

Solution 5

Code

Here's an approach using the great switch-case syntax in Swift 3/4/5:

import UIKit

extension UIWindow {
    /// Returns the currently visible view controller if any reachable within the window.
    public var visibleViewController: UIViewController? {
        return UIWindow.visibleViewController(from: rootViewController)
    }

    /// Recursively follows navigation controllers, tab bar controllers and modal presented view controllers starting
    /// from the given view controller to find the currently visible view controller.
    ///
    /// - Parameters:
    ///   - viewController: The view controller to start the recursive search from.
    /// - Returns: The view controller that is most probably visible on screen right now.
    public static func visibleViewController(from viewController: UIViewController?) -> UIViewController? {
        switch viewController {
        case let navigationController as UINavigationController:
            return UIWindow.visibleViewController(from: navigationController.visibleViewController ?? navigationController.topViewController)

        case let tabBarController as UITabBarController:
            return UIWindow.visibleViewController(from: tabBarController.selectedViewController)

        case let presentingViewController where viewController?.presentedViewController != nil:
            return UIWindow.visibleViewController(from: presentingViewController?.presentedViewController)

        default:
            return viewController
        }
    }
}

The basic idea is the same as in zirinisp's answer, it's just using a more Swift 3+ like syntax.


Usage

You probably want to create a file named UIWindowExt.swift and copy the above extension code into it.

On the call side it can be either used without any specific view controller:

if let visibleViewCtrl = UIApplication.shared.keyWindow?.visibleViewController {
    // do whatever you want with your `visibleViewCtrl`
}

Or if you know your visible view controller is reachable from a specific view controller:

if let visibleViewCtrl = UIWindow.visibleViewController(from: specificViewCtrl) {
    // do whatever you want with your `visibleViewCtrl`
}

I hope it helps!

Share:
194,044
lu yuan
Author by

lu yuan

http://www.seeyoukee.com

Updated on October 23, 2020

Comments

  • lu yuan
    lu yuan over 3 years

    The current UIViewController on the screen need to response to push-notifications from APNs, by setting some badge views. But how could I get the UIViewController in methodapplication:didReceiveRemoteNotification: of AppDelegate.m?

    I tried use self.window.rootViewController to get the current displaying UIViewController, it may be a UINavigationViewController or some other kind of view controller. And I find out that the visibleViewController property of UINavigationViewController can be used to get the UIViewController on the screen. But what could I do if it is not a UINavigationViewController?

    Any help is appreciated! The related code is as following.

    AppDelegate.m

    ...
    - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    
        //I would like to find out which view controller is on the screen here.
    
        UIViewController *vc = [(UINavigationViewController *)self.window.rootViewController visibleViewController];
        [vc performSelector:@selector(handleThePushNotification:) withObject:userInfo];
    }
    ...
    

    ViewControllerA.m

    - (void)handleThePushNotification:(NSDictionary *)userInfo{
    
        //set some badge view here
    
    }
    
  • Dima
    Dima almost 12 years
    The problem with this is if the visible view does not belong to the root view controller (in the case of modal views and such).
  • lu yuan
    lu yuan almost 12 years
    Yes, I do. But it maybe a UITabViewController. Isn't there a directly method to get the UIViewController on the screen?
  • sergio
    sergio almost 12 years
    well, you see, UINavigationController provides a way for you to know which controller is topmost; your root controller should provide the same info some way. It cannot be inferred in general because it depends strictly on how you built your UI and there is no explicit controller hierarchy (like it happens for views). You may simply add a property to your root controller and set its value whenever you "push" a new controller on top.
  • lu yuan
    lu yuan almost 12 years
    Yes, it is related to a view, as i have to show the badge view. let me check the link. thank you :)
  • Dima
    Dima almost 12 years
    As long as the value is kept up to date, that seems like a good way to go to me too.
  • lu yuan
    lu yuan almost 12 years
    I expect a method of window such as "self.window.viewOnScreen", but isn't there. lol
  • sergio
    sergio almost 12 years
    I see... :-) think that rootViewController was added first with iOS 4... i.e., before that, there was not even that available... maybe with iOS 7 you will get viewOnScreen...
  • CainaSouza
    CainaSouza about 11 years
    What is addObserver:bar inside viewDidLoad? Do I have to replace with self?
  • Aneil Mallavarapu
    Aneil Mallavarapu about 11 years
    Thanks for pointing that out - it should be self. I'll update the answer.
  • Awais Tariq
    Awais Tariq almost 11 years
    crash while getting all keys from userInfo.. Any idea? [NSConcreteNotification allKeys]: unrecognized selector sent to instance 0x1fd87480 2013-07-05 16:10:36.469 Providence[2961:907] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSConcreteNotification allKeys]: unrecognized selector sent to instance 0x1fd87480'
  • Aneil Mallavarapu
    Aneil Mallavarapu over 10 years
    @AwaisTariq - Hmmm - my guess is that the object passed by iOS to didReceiveRemoteNotification is not actually an NSDictionary, as the interface specifies.
  • Gingi
    Gingi about 10 years
    There is no direct way of getting to the controller from a UIView instance. rootViewController is not necessarily the currently shown controller. It's just at the top of the view hierarchy.
  • sergio
    sergio about 10 years
    @Gingi: you are perfectly right. Indeed, I wrote in my answer: "Once you know the root view controller, then it depends on how you have built your UI, but you can possibly find out a way to navigate through the controllers hierarchy." I had never ever thought that the rootViewController could be the currently shown controller...
  • Flexo
    Flexo almost 10 years
    Please don't duplicate answers - either flag the questions as duplicates if they are, or answer the individual questions with the specific answer they deserve if they aren't duplicates.
  • Resty
    Resty over 9 years
    Dfntly the best answer, also u can name your viewController with: self.title = myPhotoView
  • David
    David over 9 years
    Major props for the "[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];" line, thanks for your thorough answer!
  • Arben Pnishi
    Arben Pnishi about 9 years
    Check this answer if you want to get something from any UIViewController not just from the rootViewController: stackoverflow.com/a/29773513/4173671.
  • LinusGeffarth
    LinusGeffarth over 8 years
    It's as! and navigationController.visibleViewController! for Swift 2.0
  • Vijay Singh Rana
    Vijay Singh Rana over 8 years
    how can i use this for swift version?
  • zirinisp
    zirinisp over 8 years
    I cannot understand your question. Copy and paste inside your code.
  • halbano
    halbano over 8 years
    What if the user has not navigated yet to your observer class? :/
  • Mingming
    Mingming over 8 years
    What about custom container VC?
  • gadu
    gadu almost 8 years
    @Mingming it shouldn't be that hard to add an extra if to check if its the custom container VC (in the getVisibielController method) and if so return the "visible" controller, which would typically be vc.childControllers.lastObject for most custom container VC implementations (i suppose), but would depend on how its implemented.
  • Lucas van Dongen
    Lucas van Dongen almost 8 years
    I encountered this block of code in a project I inherited and I discovered that AlertControllers are now part of the hierarchy as well. You cannot present an AlertController on top of another AlertController because it's not a regular VC.
  • levigroker
    levigroker over 7 years
    This does not handle the situation where the presented view controller is a UINavigationController which has its own children.
  • jungledev
    jungledev over 7 years
    @levigroker, perhaps it's the way you've architected your views? It works fine for me to use this with a Nav. (that's how I'm using it)
  • levigroker
    levigroker over 7 years
    @jungledev I'm sure you're correct. That said, a solution which works in all view controller configurations is what is needed.
  • jungledev
    jungledev over 7 years
    @levigroker it does work in all standard vc configurations- the app I work on has a really complex architecture, is used by over 500k users, and this works everywhere in the app. Perhaps you should post a question asking why it doesn't work in your view, with code examples?
  • levigroker
    levigroker over 7 years
    jungledev I'm happy this code works for you, but it does not appear to be a complete solution. @zirinisp's answer works perfectly in my situation.
  • Pangu
    Pangu over 7 years
    I stand corrected...this seems to only work in AppDelegate for me..if I put this same function in a VC2 where VC1 segues to VC2, it only shows VC1 even though VC2 is the currently presented VC...not sure why that is
  • Aneil Mallavarapu
    Aneil Mallavarapu over 7 years
    @halbano - If the observer hasn't been loaded yet it shouldn't matter, should it? Just initialize it with fresh data when you create it.
  • jungledev
    jungledev over 7 years
    @Pangu are you using the Objective-C or Swift version? I only implemented the Obj-C version personally. I took the Swift version from another user's answer (and gave credit where it was due) but haven't tested it. I use this in over a dozen places in my app, with segues and alert views and modals, and it works- is your vc2 in a subview/container view??
  • jungledev
    jungledev over 7 years
    I did have an issue where I was showing multiple alertviews in a row to indicate the status of a file download. Sometimes files would download so fast, that alertviews were attempting to be stacked on top of each other- so the getTopVC function wouldn't allow me to properly display the alerts if there was one in process of being presented. My solution was to add the following code to be the first thing inside the while method (see next comment)
  • jungledev
    jungledev over 7 years
    // Can't present an alert view on an alert view, or an alert view on a view controller that is already presenting one. if ([topViewController.presentedViewController class] == [UIAlertController class]) { [topViewController.presentedViewController dismissViewControllerAnimated:YES completion:nil]; break; }
  • Pangu
    Pangu over 7 years
    @jungledev: I'm using the swift version, and actually it did work, but I had to call the function within viewDidLayoutSubiews to get the correct presenting ViewController. When I called the function within viewDidLoad, it always grabs the root ViewController, not sure why that's the case.
  • jungledev
    jungledev over 7 years
    @Pangu because after you load the view the first time, depending on how your app is architected, viewDidLoad doesn't always get called every time you show that view. Glad you figured it out.
  • Jeehut
    Jeehut over 7 years
    I just posted an answer with the same approach as in this answer except for an updated syntax: It's using a switch-case and follows the Swift 3 naming conventions: stackoverflow.com/a/42486823/3451975
  • Ikhsan Assaat
    Ikhsan Assaat about 7 years
    The third case will crash because of an infinite recursion. The fix is to rename the vc as presentingViewController and pass presentingViewController.presentedViewController as the parameter to the recursive method.
  • Jeehut
    Jeehut about 7 years
    I didn't quite get it, sorry. You mean UIWindow.visibleViewController(from: presentedViewController) should instead be UIWindow.visibleViewController(from: presentingViewController.presentedViewController)?
  • Ikhsan Assaat
    Ikhsan Assaat about 7 years
    correct, presentedViewController and viewController is the same object and it will call the method with itself until the stack overflows (pun intended). So it'll be case let presentingViewController where viewController?.presentedViewController != nil: return UIWindow.visibleViewController(from: presentingViewController.presentedViewController)
  • Jeff Muir
    Jeff Muir almost 7 years
    This appears to be code for Swift 2.x. Swift 3.x does not have "where" anymore. Also, "sharedApplication()" is now "shared". Not a big deal. It only takes a minute to update. Might be good to mention that it uses recursion. Also, each call to topViewController should need the "base:" prefix.
  • TM Lynch
    TM Lynch over 4 years
    This solution worked when others did not. You should update to Swift 5. Essentially no change. Just update the header for your answer.