35

I have some viewControllers, and I don't use NavigationController. How can I get visible view controller in app delegate methods (e.g. applicationWillResignActive)?

I know how to do it from NSNotification, but I think it's the wrong way.

spongebob
  • 8,370
  • 15
  • 50
  • 83
user3501341
  • 353
  • 1
  • 3
  • 4

14 Answers14

55

This should do it for you:

- (void)applicationWillResignActive:(UIApplication *)application
{
    UIViewController *vc = [self visibleViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}

- (UIViewController *)visibleViewController:(UIViewController *)rootViewController
{
    if (rootViewController.presentedViewController == nil)
    {
        return rootViewController;
    }
    if ([rootViewController.presentedViewController isKindOfClass:[UINavigationController class]])
    {
        UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
        UIViewController *lastViewController = [[navigationController viewControllers] lastObject];

        return [self visibleViewController:lastViewController];
    }
    if ([rootViewController.presentedViewController isKindOfClass:[UITabBarController class]])
    {
        UITabBarController *tabBarController = (UITabBarController *)rootViewController.presentedViewController;
        UIViewController *selectedViewController = tabBarController.selectedViewController;

        return [self visibleViewController:selectedViewController];
    }

    UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController;

    return [self visibleViewController:presentedViewController];
}
klcjr89
  • 5,862
  • 10
  • 58
  • 91
40

@aviatorken89's answer worked well for me. I had to translate it to Swift - for anybody starting out with Swift:

Updated for Swift 3:

func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {

    var rootVC = rootViewController
    if rootVC == nil {
        rootVC = UIApplication.shared.keyWindow?.rootViewController
    }

    if rootVC?.presentedViewController == nil {
        return rootVC
    }

    if let presented = rootVC?.presentedViewController {
        if presented.isKind(of: UINavigationController.self) {
            let navigationController = presented as! UINavigationController
            return navigationController.viewControllers.last!
        }

        if presented.isKind(of: UITabBarController.self) {
            let tabBarController = presented as! UITabBarController
            return tabBarController.selectedViewController!
        }

        return getVisibleViewController(presented)
    }
    return nil
}

Old answer:

func applicationWillResignActive(application: UIApplication) {
    let currentViewController = getVisibleViewController(nil)
}

func getVisibleViewController(var rootViewController: UIViewController?) -> UIViewController? {

    if rootViewController == nil {
        rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
    }

    if rootViewController?.presentedViewController == nil {
        return rootViewController
    }

    if let presented = rootViewController?.presentedViewController {
        if presented.isKindOfClass(UINavigationController) {
            let navigationController = presented as! UINavigationController
            return navigationController.viewControllers.last!
        }

        if presented.isKindOfClass(UITabBarController) {
            let tabBarController = presented as! UITabBarController
            return tabBarController.selectedViewController!
        }

        return getVisibleViewController(presented)
    }
    return nil
}
ProgrammierTier
  • 1,400
  • 10
  • 15
  • Worked like a charm! Thank you – Timo Cengiz Dec 23 '15 at 13:52
  • Cool, note that I changed `return rootViewController!` to `return rootViewController`, since it could be be nil. – ProgrammierTier Jan 06 '16 at 17:00
  • How could I check the value of `currentViewController`? For example, if I wanted to say `if currentViewController == someViewController { //do this } else { //do that}` is there a way? Printing the variable gives the name, but appends an identifier that isn't maintained between builds. – Craig.Pearce Oct 20 '16 at 04:28
  • @Craig.Pearce: You should be able to do so by invoking this function and then testing the returned value against a specific type that you expect. Something like: `let currentViewController = getVisibleViewController(nil) if currentViewController.isKindOfClass(YourViewControllerClass) { ... } else ...` – ProgrammierTier Oct 21 '16 at 17:05
  • @ProgrammierTier That got me on the right path but it didn't work since I was comparing two UIViewControllers. Instead I went with `if NSStringFromClass((currentViewController?.classForCoder)!) == "appname.theViewControllerOfInterest"....` Seems to work so far. – Craig.Pearce Oct 22 '16 at 03:02
  • 1
    isn't rootViewController always fixed as mentioned [here](http://stackoverflow.com/questions/12418177/how-to-get-root-view-controller), why do you need to take in as a parameter? – mfaani Dec 21 '16 at 16:46
  • Seems like this needs to be recursive. eg. a presented VC with a navcontroller may be presenting a VC, etc. – mxcl Jul 24 '17 at 15:49
  • return getVisibleViewController(presented) this line always returning UINavigationController not UIViewcontroller class. Please advice – Harendra Tiwari May 10 '18 at 18:37
20

We implemented it as an UIApplication extension:

import UIKit

extension UIApplication {

    var visibleViewController: UIViewController? {

        guard let rootViewController = keyWindow?.rootViewController else {
            return nil
        }

        return getVisibleViewController(rootViewController)
    }

    private func getVisibleViewController(_ rootViewController: UIViewController) -> UIViewController? {

        if let presentedViewController = rootViewController.presentedViewController {
            return getVisibleViewController(presentedViewController)
        }

        if let navigationController = rootViewController as? UINavigationController {
            return navigationController.visibleViewController
        }

        if let tabBarController = rootViewController as? UITabBarController {
            return tabBarController.selectedViewController
        }

        return rootViewController
    }
}
crelies
  • 200
  • 2
  • 5
  • 2
    Best and cleanest approach so far. – Sebastian Boldt Jul 10 '17 at 09:04
  • 1
    I found the need to alter the tabbarcontroller condition to check if the selectedViewController was a navigation controller, if so, then I get the navigation controllers visible view controller. Because nav controller can't/shouldn't contain tabor controller, the inverse check is not needed. – Jbryson Sep 18 '18 at 21:23
14

Here is an answer in Swift 4 that is very similar to the accepted answer but has a few improvements:

  1. Iterative instead of recursive.
  2. Goes all the way does the navigation stack.
  3. More "swifty" syntax.
  4. Static variable that can be put anywhere (not just in AppDelegate).
  5. Won't crash in odd cases, i.e. when a tab bar controller has no selected view controller.

    static var visibleViewController: UIViewController? {
        var currentVc = UIApplication.shared.keyWindow?.rootViewController
        while let presentedVc = currentVc?.presentedViewController {
            if let navVc = (presentedVc as? UINavigationController)?.viewControllers.last {
                currentVc = navVc
            } else if let tabVc = (presentedVc as? UITabBarController)?.selectedViewController {
                currentVc = tabVc
            } else {
                currentVc = presentedVc
            }
        }
        return currentVc
    }
    
kmell96
  • 1,365
  • 1
  • 15
  • 39
  • You can put it inside any class/struct/enum because it's a static variable. For example, if you put it inside `AppDelegate`, you would call it with `AppDelegate.visibleViewController`. – kmell96 Dec 08 '18 at 21:22
  • Does this handle navigation controllers nested inside tab bar controllers? – Balázs Vincze Dec 08 '20 at 16:00
  • Yes, it should handle it when the navigation controller is the selected VC in the tab bar controller. – kmell96 Dec 10 '20 at 21:58
5

The top recommendations here will work ok in many scenarios to get the 'best guess' solution but with a few minor adjustments, we can get a more complete solution that doesn't rely on your app's view hierarchy implementation.

1) Cocoa Touch's view hierarchy allows for multiple children to be present and visible at one time so we need to instead ask for the current visible view controllers (plural) and handle the results accordingly

2) UINavigationControllers and UITabBarControllers are commonly used in iOS applications, but they are not the only kind of container view controllers. UIKit also supplies the UIPageViewController, UISplitViewController, and allows you to write your own custom container view controllers.

3) We probably want to ignore popover modals and specific types of view controllers such UIAlertControllers or a custom embedded child-viewcontroller.

private func visibleViewControllers() -> [UIViewController] {
    guard let root = window?.rootViewController else { return [] }
    return visibleLeaves(from: root, excluding: [UIAlertController.self])
}

private func visibleLeaves(from parent: UIViewController, excluding excludedTypes: [UIViewController.Type] = []) -> [UIViewController] {

    let isExcluded: (UIViewController) -> Bool = { vc in
        excludedTypes.contains(where: { vc.isKind(of: $0) }) || vc.modalPresentationStyle == .popover
    }

    if let presented = parent.presentedViewController, !isExcluded(presented) {
        return self.visibleLeaves(from: presented, excluding: excludedTypes)
    }

    let visibleChildren = parent.childViewControllers.filter {
        $0.isViewLoaded && $0.view.window != nil
    }

    let visibleLeaves = visibleChildren.flatMap {
        return self.visibleLeaves(from: $0, excluding: excludedTypes)
    }

    if visibleLeaves.count > 0 {
        return visibleLeaves
    } else if !isExcluded(parent) {
        return [parent]
    } else {
        return []
    }
}
upsidejames
  • 51
  • 1
  • 1
4

This is an improved version of @ProgrammierTier's answer. If you have a navbar nested in a tabbar, you will get back UINavigationController using @ProgrammierTier's answer. Also, there is less force unwrapping. This should address the issue @Harendra-Tiwari is facing.

Swift 4.2:

func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {

  var rootVC = rootViewController
  if rootVC == nil {
      rootVC = UIApplication.shared.keyWindow?.rootViewController
  }

  var presented = rootVC?.presentedViewController
  if rootVC?.presentedViewController == nil {
      if let isTab = rootVC?.isKind(of: UITabBarController.self), let isNav = rootVC?.isKind(of: UINavigationController.self) {
          if !isTab && !isNav {
              return rootVC
          }
          presented = rootVC
      } else {
          return rootVC
      }
  }

  if let presented = presented {
    if presented.isKind(of: UINavigationController.self) {
        if let navigationController = presented as? UINavigationController {
            return navigationController.viewControllers.last!
        }
     }

     if presented.isKind(of: UITabBarController.self) {
        if let tabBarController = presented as? UITabBarController {
            if let navigationController = tabBarController.selectedViewController! as? UINavigationController {
                 return navigationController.viewControllers.last!
             } else {
                 return tabBarController.selectedViewController!
             }
         }
     }

     return getVisibleViewController(presented)
  }
  return nil
}
pyromancer2
  • 173
  • 1
  • 1
  • 6
3

Here is a recursive, protocol-oriented approach in Swift. Can be extended to custom types but any kind of UIViewController subclass should work with the code below.

public protocol ViewControllerContainer {

    var topMostViewController: UIViewController? { get }
}

extension UIViewController: ViewControllerContainer {

    public var topMostViewController: UIViewController? {

        if let presentedView = presentedViewController {

            return recurseViewController(presentedView)
        }

        return childViewControllers.last.map(recurseViewController)
    }
}

extension UITabBarController {

    public override var topMostViewController: UIViewController? {

        return selectedViewController.map(recurseViewController)
    }
}

extension UINavigationController {

    public override var topMostViewController: UIViewController? {

        return viewControllers.last.map(recurseViewController)
    }
}

extension UIWindow: ViewControllerContainer {

    public var topMostViewController: UIViewController? {

        return rootViewController.map(recurseViewController)
    }
}

func recurseViewController(viewController: UIViewController) -> UIViewController {

    return viewController.topMostViewController.map(recurseViewController) ?? viewController
}
Rémy Virin
  • 3,379
  • 23
  • 42
Patrick Goley
  • 5,397
  • 22
  • 42
3

If you are using IQKeyboardManager they have an extension in there

  • (UIViewController*)currentViewController;

so you can do

 application.keyWindow?.currentViewController? // <- there you go

so add this to your pod file

pod 'IQKeyboardManager'

then pod update and you are away!

hope this helps

Mark Gilchrist
  • 1,972
  • 3
  • 24
  • 44
  • keyWindow?.currentViewController? gives error, unable to find this variable/method in IQKeyboardManagerSwift. – Haris Dec 01 '22 at 13:25
2

If your app's root view controller is a UINavigationController than you can use this:

UIViewController *currentControllerName = ((UINavigationController*)appDelegate.window.rootViewController).visibleViewController;

and if you are using UITabBarController than you can use this:

UIViewController *currentControllerName = ((UITabBarController*)appDelegate.window.rootViewController).selectedViewController;
0

Here's a Swift 2.3 implementation of @ProgrammierTier's answer as an extension to a UIViewController

extension UIViewController {
    var visibleViewController: UIViewController? {
        if presentedViewController == nil {
            return self
        }

        if let presented = presentedViewController {
            if presented.isKindOfClass(UINavigationController) {
                let navigationController = presented as! UINavigationController
                return navigationController.viewControllers.last
            }

            if presented.isKindOfClass(UITabBarController) {
                let tabBarController = presented as! UITabBarController
                return tabBarController.selectedViewController
            }

            return presented.visibleViewController
        }

        return nil
    }
}

To get it from applicationWillResignActive

func applicationWillResignActive(application: UIApplication) {
    let visibleVC = application.keyWindow?.rootViewController?.visibleViewController
}
Zonily Jame
  • 5,053
  • 3
  • 30
  • 56
0

In my case i have Tabbar controller and then Navigation controller for each Tab hope it helps someone

 UIViewController *loginViewController = self.window.rootViewController;
    
 UITabBarController *controller = loginViewController.tabBarController;
    
 UIViewController *currentController = controller.selectedViewController.childViewControllers.lastObject;
Balázs Vincze
  • 3,550
  • 5
  • 29
  • 60
Manvir Singh
  • 129
  • 2
  • 11
0

Here's just a quick fix inspired from @krcjr89's answer. The accepted answer doesn't go all the way down the navigation. For instance, if you have a navigation controller embedded in tab bar controller, you won't get to the visible view controller but the navigation controller.

I made it an extension of UIApplication like @Christian, as this makes the most sense.

extension UIApplication {
    var visibleViewController: UIViewController? {
        return getVisibleViewController(nil)
    }

    private func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {

        let rootVC = rootViewController ?? UIApplication.shared.keyWindow?.rootViewController

        if rootVC!.isKind(of: UINavigationController.self) {
            let navigationController = rootVC as! UINavigationController
            return getVisibleViewController(navigationController.viewControllers.last!)
        }

        if rootVC!.isKind(of: UITabBarController.self) {
            let tabBarController = rootVC as! UITabBarController
            return getVisibleViewController(tabBarController.selectedViewController!)
        }

        if let presentedVC = rootVC?.presentedViewController {
            return getVisibleViewController(presentedVC)
        }

        return rootVC
    }
}
Ambroise Collon
  • 3,839
  • 3
  • 18
  • 37
0

A modified version of a previous answer using UIViewController category in ObjC:

UIViewController+VisibleViewController.h

#import <UIKit/UIKit.h>

@interface UIViewController (VisibleViewController)

- (UIViewController *)visibleViewController;

@end

UIViewController+VisibleViewController.m

#import "UIViewController+VisibleViewController.h"

@implementation UIViewController (VisibleViewController)

- (UIViewController *)visibleViewController {
    if (self.presentedViewController == nil) {
        return self;
    }
    if ([self.presentedViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController *navigationController = (UINavigationController *)self.presentedViewController;
        UIViewController *lastViewController = [[navigationController viewControllers] lastObject];

        return [lastViewController visibleViewController];
    }
    if ([self.presentedViewController isKindOfClass:[UITabBarController class]]) {
        UITabBarController *tabBarController = (UITabBarController *)self.presentedViewController;
        UIViewController *selectedViewController = tabBarController.selectedViewController;

        return [selectedViewController visibleViewController];
    }

    UIViewController *presentedViewController = (UIViewController *)self.presentedViewController;

    return [presentedViewController visibleViewController];
}

@end

AppDelegate.m

#import "UIViewController+VisibleViewController.h"

- (UIViewController *) applicationVisibleViewController {
    return [self.window.rootViewController visibleViewController];
}
Raunak
  • 3,314
  • 1
  • 22
  • 28
-1

modified from troop231

+ (UIViewController *)visibleViewController:(UIViewController *)rootViewController
{
    if ([rootViewController isKindOfClass:[UINavigationController class]])
    {
        UINavigationController *navigationController = (UINavigationController *)rootViewController;
        UIViewController *lastViewController = [[navigationController viewControllers] lastObject];

        return [self visibleViewController:lastViewController];
    }
    if ([rootViewController isKindOfClass:[UITabBarController class]])
    {
        UITabBarController *tabBarController = (UITabBarController *)rootViewController;
        UIViewController *selectedViewController = tabBarController.selectedViewController;

        return [self visibleViewController:selectedViewController];
    }

    if (rootViewController.presentedViewController != nil)
    {
        UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController;
        return [self visibleViewController:presentedViewController];
    }

    return rootViewController;
}
amoblin
  • 11
  • 1