Get the current displaying UIViewController on the screen in AppDelegate.m
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!
![lu yuan](https://i.stack.imgur.com/n4RKt.jpg?s=256&g=1)
Comments
-
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 theUIViewController
in methodapplication:didReceiveRemoteNotification
: ofAppDelegate.m
?I tried use
self.window.rootViewController
to get the current displayingUIViewController
, it may be aUINavigationViewController
or some other kind of view controller. And I find out that thevisibleViewController
property ofUINavigationViewController
can be used to get theUIViewController
on the screen. But what could I do if it is not aUINavigationViewController
?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 almost 12 yearsThe 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 almost 12 yearsYes, I do. But it maybe a UITabViewController. Isn't there a directly method to get the UIViewController on the screen?
-
sergio almost 12 yearswell, 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 almost 12 yearsYes, it is related to a view, as i have to show the badge view. let me check the link. thank you :)
-
Dima almost 12 yearsAs long as the value is kept up to date, that seems like a good way to go to me too.
-
lu yuan almost 12 yearsI expect a method of window such as "self.window.viewOnScreen", but isn't there. lol
-
sergio almost 12 yearsI 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 about 11 yearsWhat is
addObserver:bar
insideviewDidLoad
? Do I have to replace withself
? -
Aneil Mallavarapu about 11 yearsThanks for pointing that out - it should be self. I'll update the answer.
-
Awais Tariq almost 11 yearscrash 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 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 about 10 yearsThere 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 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 almost 10 yearsPlease 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 over 9 yearsDfntly the best answer, also u can name your viewController with:
self.title = myPhotoView
-
David over 9 yearsMajor props for the "[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];" line, thanks for your thorough answer!
-
Arben Pnishi about 9 yearsCheck this answer if you want to get something from any UIViewController not just from the rootViewController: stackoverflow.com/a/29773513/4173671.
-
LinusGeffarth over 8 yearsIt's
as!
andnavigationController.visibleViewController!
for Swift 2.0 -
Vijay Singh Rana over 8 yearshow can i use this for swift version?
-
zirinisp over 8 yearsI cannot understand your question. Copy and paste inside your code.
-
halbano over 8 yearsWhat if the user has not navigated yet to your observer class? :/
-
Mingming over 8 yearsWhat about custom container VC?
-
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 almost 8 yearsI 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 over 7 yearsThis does not handle the situation where the presented view controller is a
UINavigationController
which has its own children. -
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 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 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 over 7 yearsjungledev 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 over 7 yearsI 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 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 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 over 7 yearsI 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 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 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 presentingViewController
. When I called the function withinviewDidLoad
, it always grabs the rootViewController
, not sure why that's the case. -
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 over 7 yearsI 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 about 7 yearsThe third case will crash because of an infinite recursion. The fix is to rename the vc as
presentingViewController
and passpresentingViewController.presentedViewController
as the parameter to the recursive method. -
Jeehut about 7 yearsI didn't quite get it, sorry. You mean
UIWindow.visibleViewController(from: presentedViewController)
should instead beUIWindow.visibleViewController(from: presentingViewController.presentedViewController)
? -
Ikhsan Assaat about 7 yearscorrect,
presentedViewController
andviewController
is the same object and it will call the method with itself until the stack overflows (pun intended). So it'll becase let presentingViewController where viewController?.presentedViewController != nil: return UIWindow.visibleViewController(from: presentingViewController.presentedViewController)
-
Jeff Muir almost 7 yearsThis 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 over 4 yearsThis solution worked when others did not. You should update to Swift 5. Essentially no change. Just update the header for your answer.