Luda
Luda

Reputation: 7068

Top most ViewController under UIAlertController

I am using the following extension to find the top most ViewController. If alert is presented, the code above gives UIAlertController. How do I get top view controller under UIAlertController?

Upvotes: 6

Views: 4333

Answers (6)

iOSDev
iOSDev

Reputation: 123

Create an UIApplication extension like below and UIApplication.topViewController() will return the top most UIViewController under UIAlertController

iOS 13+

extension UIApplication {

    class func topViewController(controller: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? {
        if let navigationController = controller as? UINavigationController {
            return topViewController(controller: navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController {
            if let selected = tabController.selectedViewController {
                return topViewController(controller: selected)
            }
        }
        if let presented = controller?.presentedViewController {
            return topViewController(controller: presented)
        }
        if let alert = controller as? UIAlertController {
            if let navigationController = alert.presentingViewController as? UINavigationController {
                return navigationController.viewControllers.last
            }
            return alert.presentingViewController
        }
        return controller
    }

}

iOS 12-

extension UIApplication {

    class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let navigationController = controller as? UINavigationController {
            return topViewController(controller: navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController {
            if let selected = tabController.selectedViewController {
                return topViewController(controller: selected)
            }
        }
        if let presented = controller?.presentedViewController {
            return topViewController(controller: presented)
        }
        if let alert = controller as? UIAlertController {
            if let navigationController = alert.presentingViewController as? UINavigationController {
                return navigationController.viewControllers.last
            }
            return alert.presentingViewController
        }
        return controller
    }

}

Upvotes: 5

Yunus Tek
Yunus Tek

Reputation: 626

This is the correct one:

    func firstApplicableViewController() -> UIViewController? {
        if (self is UITabBarController) {
            let tabBarController = self as? UITabBarController
            return tabBarController?.selectedViewController?.firstApplicableViewController()
        } else if (self is UINavigationController) {
            let navigationController = self as? UINavigationController
            return navigationController?.visibleViewController?.firstApplicableViewController()
        } else if (self is UIAlertController) {
            let presentingViewController: UIViewController = self.presentingViewController!
            return presentingViewController.firstApplicableViewController()
        } else if self.presentedViewController != nil {
            let presentedViewController: UIViewController = self.presentedViewController!
            if (presentedViewController is UIAlertController) {
                return self
            } else {
                return presentedViewController.firstApplicableViewController()
            }
        } else {
            return self
        }
    }

Upvotes: 0

merito
merito

Reputation: 485

I think you want to push a new VC on current top visible VC which is a UIAlertController, then this UIAlertController will disappear immediately, cause pushed new VC dismiss too. Finally, you can not push a new VC.

The problem is, if you new a UIAlertView, then call show, Cocoa Touch will initialize a new window which rootViewController is UIApplicationRotationFollowingController which presentingViewController is UIAlertController. So you cannot traverse the top most VC under UIAlertController because it exist in another window!

So if topViewController traverse from keyWindow?.rootViewController, find a UIAlertController, call topViewController again but traverse from window what you want, such as (UIApplication.sharedApplication().delegate as! AppDelegate).window?.rootViewController

Upvotes: 0

Jelly
Jelly

Reputation: 4532

You could check if the next viewController is UIAlertController and if so return its parent. Something like this:

if let presented = base as? UIAlertController {
  return base.presentingViewController
}

Add this in the extension you use before return.

Updated

extension UIApplication {
   class func topViewController(base: UIViewController? =    (UIApplication.sharedApplication().delegate as! AppDelegate).window?.rootViewController) -> UIViewController? {
      if let nav = base as? UINavigationController {
         return topViewController(base: nav.visibleViewController)
      }
      if let tab = base as? UITabBarController {
         if let selected = tab.selectedViewController {
             return topViewController(base: selected)
         }
      }
      if let presented = base?.presentedViewController {
         return topViewController(base: presented)
      }

      if let alert = base as? UIAlertController {
         return alert.presentingViewController
      }

      return base
   }
}

Upvotes: 1

odm
odm

Reputation: 906

I used this extension to get the top most view controller under an UIAlertController, basically what I do is to stop looking for top view controller when I found one that is an UIAlertController.

extension UIApplication {

var topViewController: UIViewController? {
    var viewController = keyWindow?.rootViewController
    guard viewController != nil else { return nil }
    var presentedViewController = viewController?.presentedViewController
    while presentedViewController != nil, !(presentedViewController is UIAlertController) {
        switch presentedViewController {
        case let navagationController as UINavigationController:
            viewController = navagationController.viewControllers.last
        case let tabBarController as UITabBarController:
            viewController = tabBarController.selectedViewController
        default:
            viewController = viewController?.presentedViewController
        }
        presentedViewController = viewController?.presentedViewController
    }
    return viewController
}

}

Upvotes: 0

atulkhatri
atulkhatri

Reputation: 11343

You can get the parent controller of UIAlertController using its presentingViewController property

extension UIApplication {
  class func topViewController(base: UIViewController? = (UIApplication.sharedApplication().delegate as! AppDelegate).window?.rootViewController) -> UIViewController? {
    if let nav = base as? UINavigationController {
      return topViewController(base: nav.visibleViewController)
    }
    if let tab = base as? UITabBarController {
      if let selected = tab.selectedViewController {
        return topViewController(base: selected)
      }
    }
    if let alert = base as? UIAlertController {
      if let presenting = alert.presentingViewController {
        return topViewController(base: presenting)
      }
    }
    if let presented = base?.presentedViewController {
      return topViewController(base: presented)
    }
    return base
  }
}

Use these changes in your code, Not tested on XCode.

Upvotes: 1

Related Questions