Get the current displaying UIViewController on the screen in AppDelegate.m
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
}
}
}
}
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...
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
}
}
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.
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!