使用故事板时,如何从应用程序委托获取可见的视图控制器?

35

我有一些viewControllers,但我不使用NavigationController。如何在应用程序委托方法(例如applicationWillResignActive)中获取可见的视图控制器?

我知道如何通过NSNotification来实现,但我认为这是错误的方式。

14个回答

55
这应该可以为您解决问题:
- (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];
}

1
而当根视图控制器是导航控制器或选项卡控制器时呢? - nhgrif
1
他不使用选项卡控制器吗?此外,模态呈现的视图怎么办?这将返回根视图,它不会是一个以模态方式呈现的视图。 - nhgrif
2
也许吧。但是你的回答应该回答所问的问题,而不是其他的。 - nhgrif
谢谢您的回答,但我也没有使用tabBarController。在这种情况下,我该如何获取可见的视图控制器? - user3501341
1
非常感谢您! - Shubham Naik
显示剩余3条评论

40

@aviatorken89的回答对我很有用。 我需要将其转换为Swift - 对于任何刚开始学习Swift的人:

已更新为Swift 3:


已更新至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
}

旧回答:

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
}

运行得非常好!谢谢。 - Timo Cengiz
注意,我将return rootViewController!更改为return rootViewController,因为它可能为空。 - ProgrammierTier
@ProgrammierTier 这让我找到了正确的方向,但它并没有起作用,因为我正在比较两个UIViewControllers。相反,我使用了 if NSStringFromClass((currentViewController?.classForCoder)!) == "appname.theViewControllerOfInterest".... 到目前为止似乎可以工作。 - Craig.Pearce
1
根据这里所述,rootViewController不是一直固定的吗?为什么需要将其作为参数传入? - mfaani
似乎需要使用递归。例如,一个带有导航控制器的已呈现视图控制器可能正在呈现另一个视图控制器等等。 - mxcl
显示剩余3条评论

20
我们将其实现为UIApplication扩展:
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
    }
}

2
到目前为止,最好、最干净的方法。 - Sebastian Boldt
1
我发现需要更改tabbarcontroller的条件,以检查selectedViewController是否为导航控制器,如果是,则获取导航控制器的可见视图控制器。因为导航控制器不能/不应该包含tabbar控制器,所以不需要反向检查。 - Jbryson

14

这是一个用Swift 4编写的答案,非常类似于被接受的答案,但有一些改进:

  1. 迭代而不是递归。
  2. 遍历整个导航堆栈。
  3. 更加 "Swifty" 的语法。
  4. 可以放置在任何地方的静态变量(而不仅仅是在AppDelegate中)。
  5. 在奇怪的情况下不会崩溃,例如当选定的视图控制器为空时。

    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
    }
    

你可以将它放在任何类/结构体/枚举中,因为它是一个静态变量。例如,如果你将它放在 AppDelegate 中,你可以通过 AppDelegate.visibleViewController 调用它。 - kmell96
这个处理嵌套在标签栏控制器中的导航控制器吗? - Balázs Vincze
是的,当导航控制器是选定的标签栏控制器中的VC时,它应该处理它。 - kmell96

5
这里的顶级建议在许多情况下可以获得“最佳猜测”解决方案,但是通过进行一些小的调整,我们可以获得更完整的解决方案,而不依赖于您的应用程序视图层次结构实现。
1)Cocoa Touch的视图层次结构允许同时存在多个子视图并且可见,因此我们需要请求当前可见的视图控制器(复数形式),并相应地处理结果。
2)UINavigationController和UITabBarController通常在iOS应用程序中使用,但它们不是唯一的容器视图控制器。UIKit还提供了UIPageViewController、UISplitViewController,并允许您编写自己的自定义容器视图控制器。
3)我们可能希望忽略弹出模态框以及UIAlertController等特定类型的视图控制器或自定义嵌入的子视图控制器。
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 []
    }
}

4
这是@ProgrammierTier答案的改进版本。如果您在选项卡栏中嵌套了导航栏,则可以使用@ProgrammierTier的答案返回UINavigationController。此外,强制解包数量较少。这应该解决@Harendra-Tiwari面临的问题。
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
}

3

如果您正在使用IQKeyboardManager,它们在其中有一个扩展

  • (UIViewController*)currentViewController;

所以您可以这样做

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

因此,请将以下内容添加到您的pod文件中

pod 'IQKeyboardManager'

then pod update and you are away!

希望这可以帮助到您。

keyWindow?.currentViewController? 在 IQKeyboardManagerSwift 中找不到此变量/方法,导致出现错误。 - Haris

3

这里是一种递归的,基于协议的 Swift 实现。它可以扩展到自定义类型,但任何一种 UIViewController 子类都应该能够使用下面的代码。

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
}

2
如果你的应用程序的根视图控制器是一个UINavigationController,那么可以使用以下代码:
UIViewController *currentControllerName = ((UINavigationController*)appDelegate.window.rootViewController).visibleViewController;

如果您正在使用UITabBarController,则可以使用以下方法:

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

0
这里只是一个快速修复,灵感来自于@krcjr89的答案。被接受的答案没有完全沿着导航走到底。例如,如果您在选项卡栏控制器中嵌入了导航控制器,则不会到达可见的视图控制器,而是导航控制器。
我将其作为UIApplication的扩展进行了修改,就像@Christian一样,因为这是最有意义的。
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
    }
}

如何在应用程序委托类中使用此内容。 - Pramod Shukla
很遗憾,UIApplication.shared.keyWindow在iOS 13.0中已被弃用。 - Cosmtar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接