Get the current displaying UIViewController on the screen in AppDelegate.m
Asked Answered
K

20

137

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

}
Kitkitchen answered 24/7, 2012 at 19:9 Comment(0)
S
105

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...

Seasoning answered 24/7, 2012 at 19:14 Comment(10)
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).Unsling
Yes, I do. But it maybe a UITabViewController. Isn't there a directly method to get the UIViewController on the screen?Kitkitchen
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.Seasoning
As long as the value is kept up to date, that seems like a good way to go to me too.Unsling
I expect a method of window such as "self.window.viewOnScreen", but isn't there. lolKitkitchen
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...Seasoning
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.Albright
@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...Seasoning
Major props for the "[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];" line, thanks for your thorough answer!Merriemerrielle
Check this answer if you want to get something from any UIViewController not just from the rootViewController: https://mcmap.net/q/168388/-how-do-i-access-my-viewcontroller-from-my-appdelegate-ios.Zoography
K
111

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
            }
        }
    }
}
Kassa answered 18/2, 2014 at 8:41 Comment(6)
how can i use this for swift version?Gradeigh
I cannot understand your question. Copy and paste inside your code.Kassa
What about custom container VC?Clause
@Clause 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.Sargent
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.Poser
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: https://mcmap.net/q/166381/-get-the-current-displaying-uiviewcontroller-on-the-screen-in-appdelegate-mPyrargyrite
S
105

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...

Seasoning answered 24/7, 2012 at 19:14 Comment(10)
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).Unsling
Yes, I do. But it maybe a UITabViewController. Isn't there a directly method to get the UIViewController on the screen?Kitkitchen
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.Seasoning
As long as the value is kept up to date, that seems like a good way to go to me too.Unsling
I expect a method of window such as "self.window.viewOnScreen", but isn't there. lolKitkitchen
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...Seasoning
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.Albright
@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...Seasoning
Major props for the "[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];" line, thanks for your thorough answer!Merriemerrielle
Check this answer if you want to get something from any UIViewController not just from the rootViewController: https://mcmap.net/q/168388/-how-do-i-access-my-viewcontroller-from-my-appdelegate-ios.Zoography
F
49

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
    }
}
Fulviah answered 24/2, 2015 at 14:59 Comment(1)
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.Absher
B
38

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 https://mcmap.net/q/63943/-how-to-tell-if-uiviewcontroller-39-s-view-is-visible
   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.

Bandwidth answered 21/4, 2013 at 14:52 Comment(6)
What is addObserver:bar inside viewDidLoad? Do I have to replace with self?Solent
Thanks for pointing that out - it should be self. I'll update the answer.Bandwidth
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'Suprarenal
@AwaisTariq - Hmmm - my guess is that the object passed by iOS to didReceiveRemoteNotification is not actually an NSDictionary, as the interface specifies.Bandwidth
What if the user has not navigated yet to your observer class? :/Shyster
@Shyster - If the observer hasn't been loaded yet it shouldn't matter, should it? Just initialize it with fresh data when you create it.Bandwidth
P
25

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!

Pyrargyrite answered 27/2, 2017 at 13:19 Comment(4)
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.Ungual
I didn't quite get it, sorry. You mean UIWindow.visibleViewController(from: presentedViewController) should instead be UIWindow.visibleViewController(from: presentingViewController.presentedViewController)?Pyrargyrite
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)Ungual
This solution worked when others did not. You should update to Swift 5. Essentially no change. Just update the header for your answer.Minimalist
M
14

I have found that iOS 8 has screwed everything up. In iOS 7 there is a new UITransitionView on the view hierarchy whenever you have a modally presented UINavigationController. Anyway, here's my code that finds gets the topmost VC. Calling getTopMostViewController should return a VC that you should be able to send a message like presentViewController:animated:completion. It's purpose is to get you a VC that you can use to present a modal VC, so it will most likely stop and return at container classes like UINavigationController and NOT the VC contained within them. Should not be hard to adapt the code to do that too. I've tested this code in various situations in iOS 6, 7 and 8. Please let me know if you find bugs.

+ (UIViewController*) getTopMostViewController
{
    UIWindow *window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal) {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(window in windows) {
            if (window.windowLevel == UIWindowLevelNormal) {
                break;
            }
        }
    }

    for (UIView *subView in [window subviews])
    {
        UIResponder *responder = [subView nextResponder];

        //added this block of code for iOS 8 which puts a UITransitionView in between the UIWindow and the UILayoutContainerView
        if ([responder isEqual:window])
        {
            //this is a UITransitionView
            if ([[subView subviews] count])
            {
                UIView *subSubView = [subView subviews][0]; //this should be the UILayoutContainerView
                responder = [subSubView nextResponder];
            }
        }

        if([responder isKindOfClass:[UIViewController class]]) {
            return [self topViewController: (UIViewController *) responder];
        }
    }

    return nil;
}

+ (UIViewController *) topViewController: (UIViewController *) controller
{
    BOOL isPresenting = NO;
    do {
        // this path is called only on iOS 6+, so -presentedViewController is fine here.
        UIViewController *presented = [controller presentedViewController];
        isPresenting = presented != nil;
        if(presented != nil) {
            controller = presented;
        }

    } while (isPresenting);

    return controller;
}
Malt answered 19/9, 2014 at 23:15 Comment(1)
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.Chemo
A
14

Way less code than all other solutions:

Objective-C version:

- (UIViewController *)getTopViewController {
    UIViewController *topViewController = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (topViewController.presentedViewController) topViewController = topViewController.presentedViewController;

    return topViewController;
}

Swift 2.0 version: (credit goes to Steve.B)

func getTopViewController() -> UIViewController {
    var topViewController = UIApplication.sharedApplication().delegate!.window!!.rootViewController!
    while (topViewController.presentedViewController != nil) {
        topViewController = topViewController.presentedViewController!
    }
    return topViewController
}

Works anywhere in your app, even with modals.

Aramenta answered 4/6, 2016 at 18:40 Comment(11)
This does not handle the situation where the presented view controller is a UINavigationController which has its own children.Godhood
@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)Aramenta
@Aramenta I'm sure you're correct. That said, a solution which works in all view controller configurations is what is needed.Godhood
@Godhood 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?Aramenta
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.Godhood
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 isCulbreth
@Culbreth 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??Aramenta
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)Aramenta
// 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; }Aramenta
@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.Culbreth
@Culbreth 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.Aramenta
H
8

zirinisp's Answer in Swift:

extension UIWindow {

    func visibleViewController() -> UIViewController? {
        if let rootViewController: UIViewController  = self.rootViewController {
            return UIWindow.getVisibleViewControllerFrom(rootViewController)
        }
        return nil
    }

    class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {

        if vc.isKindOfClass(UINavigationController.self) {

            let navigationController = vc as UINavigationController
            return UIWindow.getVisibleViewControllerFrom( navigationController.visibleViewController)

        } else if vc.isKindOfClass(UITabBarController.self) {

            let tabBarController = vc as UITabBarController
            return UIWindow.getVisibleViewControllerFrom(tabBarController.selectedViewController!)

        } else {

            if let presentedViewController = vc.presentedViewController {

                return UIWindow.getVisibleViewControllerFrom(presentedViewController.presentedViewController!)

            } else {

                return vc;
            }
        }
    }
}

Usage:

 if let topController = window.visibleViewController() {
            println(topController)
        }
Hydride answered 19/12, 2014 at 14:59 Comment(1)
It's as! and navigationController.visibleViewController! for Swift 2.0Cerebro
T
7

Specify title to each ViewController and then get the title of current ViewController by the code given below.

-(void)viewDidUnload {
  NSString *currentController = self.navigationController.visibleViewController.title;

Then check it by your title like this

  if([currentController isEqualToString:@"myViewControllerTitle"]){
    //write your code according to View controller.
  }
}
Talanian answered 19/8, 2013 at 9:19 Comment(1)
Dfntly the best answer, also u can name your viewController with: self.title = myPhotoViewAgraphia
R
5

Mine is better! :)

extension UIApplication {
    var visibleViewController : UIViewController? {
        return keyWindow?.rootViewController?.topViewController
    }
}

extension UIViewController {
    fileprivate var topViewController: UIViewController {
        switch self {
        case is UINavigationController:
            return (self as! UINavigationController).visibleViewController?.topViewController ?? self
        case is UITabBarController:
            return (self as! UITabBarController).selectedViewController?.topViewController ?? self
        default:
            return presentedViewController?.topViewController ?? self
        }
    }
}
Rimbaud answered 11/9, 2017 at 19:47 Comment(0)
U
4

Why not just handle the push notification code in the app delegate? Is it directly related to a view?

You can check if a UIViewController's view is currently visible by checking if it's view's window property has a value. See more here.

Unsling answered 24/7, 2012 at 19:16 Comment(1)
Yes, it is related to a view, as i have to show the badge view. let me check the link. thank you :)Kitkitchen
T
4

Just addition to @zirinisp answer.

Create a file, name it UIWindowExtension.swift and paste the following snippet:

import UIKit

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
            }
        }
    }
}

func getTopViewController() -> UIViewController? {
    let appDelegate = UIApplication.sharedApplication().delegate
    if let window = appDelegate!.window {
        return window?.visibleViewController
    }
    return nil
}

Use it anywhere as:

if let topVC = getTopViewController() {

}

Thanks to @zirinisp.

Tessie answered 3/3, 2016 at 13:34 Comment(0)
F
3

Regarding NSNotificationCenter Post above (sorry can't find out where to post a comment under it...)

In case some were getting the -[NSConcreteNotification allKeys] error of sorts. Change this:

-(void)didReceiveRemoteNotification:(NSDictionary *)userInfo

to this:

-(void)didReceiveRemoteNotification:(NSNotification*)notif {
NSDictionary *dict = notif.userInfo;
}
Fennie answered 13/11, 2013 at 16:57 Comment(0)
L
3

This worked for me. I have many targets that have different controllers so previous answers didn't seemed to work.

first you want this inside your AppDelegate class:

var window: UIWindow?

then, in your function

let navigationController = window?.rootViewController as? UINavigationController
if let activeController = navigationController!.visibleViewController {
    if activeController.isKindOfClass( MyViewController )  {
        println("I have found my controller!")    
   }
}
Landrum answered 23/4, 2015 at 19:57 Comment(0)
A
3

This is the best possible way that I have tried out. If it should help anyone...

+ (UIViewController*) topMostController
{
    UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;

    while (topController.presentedViewController) {
        topController = topController.presentedViewController;
    }

    return topController;
}
Antitank answered 23/11, 2016 at 9:51 Comment(0)
H
2

I created a category for UIApplication with visibleViewControllers property. The main idea is pretty simple. I swizzled viewDidAppear and viewDidDisappear methods in UIViewController. In viewDidAppear method viewController is added to stack. In viewDidDisappear method viewController is removed from stack. NSPointerArray is used instead of NSArray to store weak UIViewController’s references . This approach works for any viewControllers hierarchy.

UIApplication+VisibleViewControllers.h

#import <UIKit/UIKit.h>

@interface UIApplication (VisibleViewControllers)

@property (nonatomic, readonly) NSArray<__kindof UIViewController *> *visibleViewControllers;

@end

UIApplication+VisibleViewControllers.m

#import "UIApplication+VisibleViewControllers.h"
#import <objc/runtime.h>

@interface UIApplication ()

@property (nonatomic, readonly) NSPointerArray *visibleViewControllersPointers;

@end

@implementation UIApplication (VisibleViewControllers)

- (NSArray<__kindof UIViewController *> *)visibleViewControllers {
    return self.visibleViewControllersPointers.allObjects;
}

- (NSPointerArray *)visibleViewControllersPointers {
    NSPointerArray *pointers = objc_getAssociatedObject(self, @selector(visibleViewControllersPointers));
    if (!pointers) {
        pointers = [NSPointerArray weakObjectsPointerArray];
        objc_setAssociatedObject(self, @selector(visibleViewControllersPointers), pointers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return pointers;
}

@end

@implementation UIViewController (UIApplication_VisibleViewControllers)

+ (void)swizzleMethodWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector {
    Method originalMethod = class_getInstanceMethod(self, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
    BOOL didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
        class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleMethodWithOriginalSelector:@selector(viewDidAppear:)
                               swizzledSelector:@selector(uiapplication_visibleviewcontrollers_viewDidAppear:)];
        [self swizzleMethodWithOriginalSelector:@selector(viewDidDisappear:)
                               swizzledSelector:@selector(uiapplication_visibleviewcontrollers_viewDidDisappear:)];
    });
}

- (void)uiapplication_visibleviewcontrollers_viewDidAppear:(BOOL)animated {
    [[UIApplication sharedApplication].visibleViewControllersPointers addPointer:(__bridge void * _Nullable)self];
    [self uiapplication_visibleviewcontrollers_viewDidAppear:animated];
}

- (void)uiapplication_visibleviewcontrollers_viewDidDisappear:(BOOL)animated {
    NSPointerArray *pointers = [UIApplication sharedApplication].visibleViewControllersPointers;
    for (int i = 0; i < pointers.count; i++) {
        UIViewController *viewController = [pointers pointerAtIndex:i];
        if ([viewController isEqual:self]) {
            [pointers removePointerAtIndex:i];
            break;
        }
    }
    [self uiapplication_visibleviewcontrollers_viewDidDisappear:animated];
}

@end

https://gist.github.com/medvedzzz/e6287b99011f2437ac0beb5a72a897f0

Swift 3 version

UIApplication+VisibleViewControllers.swift

import UIKit

extension UIApplication {

    private struct AssociatedObjectsKeys {
        static var visibleViewControllersPointers = "UIApplication_visibleViewControllersPointers"
    }

    fileprivate var visibleViewControllersPointers: NSPointerArray {
        var pointers = objc_getAssociatedObject(self, &AssociatedObjectsKeys.visibleViewControllersPointers) as! NSPointerArray?
        if (pointers == nil) {
            pointers = NSPointerArray.weakObjects()
            objc_setAssociatedObject(self, &AssociatedObjectsKeys.visibleViewControllersPointers, pointers, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        return pointers!
    }

    var visibleViewControllers: [UIViewController] {
        return visibleViewControllersPointers.allObjects as! [UIViewController]
    }
}

extension UIViewController {

    private static func swizzleFunc(withOriginalSelector originalSelector: Selector, swizzledSelector: Selector) {
        let originalMethod = class_getInstanceMethod(self, originalSelector)
        let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
        let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
        if didAddMethod {
            class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }

    override open class func initialize() {
        if self != UIViewController.self {
            return
        }
        let swizzlingClosure: () = {
            UIViewController.swizzleFunc(withOriginalSelector: #selector(UIViewController.viewDidAppear(_:)),
                                         swizzledSelector: #selector(uiapplication_visibleviewcontrollers_viewDidAppear(_:)))
            UIViewController.swizzleFunc(withOriginalSelector: #selector(UIViewController.viewDidDisappear(_:)),
                                         swizzledSelector: #selector(uiapplication_visibleviewcontrollers_viewDidDisappear(_:)))
        }()
        swizzlingClosure
    }

    @objc private func uiapplication_visibleviewcontrollers_viewDidAppear(_ animated: Bool) {
        UIApplication.shared.visibleViewControllersPointers.addPointer(Unmanaged.passUnretained(self).toOpaque())
        uiapplication_visibleviewcontrollers_viewDidAppear(animated)
    }

    @objc private func uiapplication_visibleviewcontrollers_viewDidDisappear(_ animated: Bool) {
        let pointers = UIApplication.shared.visibleViewControllersPointers
        for i in 0..<pointers.count {
            if let pointer = pointers.pointer(at: i) {
                let viewController = Unmanaged<AnyObject>.fromOpaque(pointer).takeUnretainedValue() as? UIViewController
                if viewController.isEqual(self) {
                    pointers.removePointer(at: i)
                    break
                }
            }
        }
        uiapplication_visibleviewcontrollers_viewDidDisappear(animated)
    }
}

https://gist.github.com/medvedzzz/ee6f4071639d987793977dba04e11399

Humorist answered 28/11, 2016 at 10:8 Comment(0)
P
2
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 easily get the top post view controller like so

let viewController = UIApplication.topMostViewController

One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController.

Pastoral answered 3/8, 2017 at 0:58 Comment(0)
F
1

Swift 2.0 version of jungledev's answer

func getTopViewController() -> UIViewController {
    var topViewController = UIApplication.sharedApplication().delegate!.window!!.rootViewController!
    while (topViewController.presentedViewController != nil) {
        topViewController = topViewController.presentedViewController!
    }
    return topViewController
}
Frederickson answered 30/8, 2016 at 14:24 Comment(0)
E
0

Always check your build configuration if you you are running your app with debug or release.

IMPORTANT NOTE: You can't be able to test it without running your app in debug mode

This was my solution

Ermina answered 20/4, 2018 at 11:46 Comment(0)
C
0

In Swift, just add this single line of code to your BaseViewController to log your displayed view controllers

override public func viewDidLoad() {

super.viewDidLoad()

print("Current ViewController 🙋🏻‍♂️  #\(UIApplication.getTopViewController()?.classForCoder ?? self.classForCoder)")

}

Crosse answered 14/3, 2022 at 6:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.