如何在iOS上找到最顶层的视图控制器

285

我遇到了几种情况,需要找到“最上层”的视图控制器(负责当前视图的视图控制器),但没有找到方法。

基本上,这个挑战是这样的:假设一个类不是视图控制器(或视图)[并且没有活动视图的地址],也没有传入最上面视图控制器的地址(或者比如导航控制器的地址),那么是否可能找到该视图控制器呢?(如果可以,怎么实现?)

或者,如果找不到它,是否可能找到最上层的视图?


那么你的意思是说这是不可能的。 - Hot Licks
@Daniel 不,我的意思是你的代码似乎需要重新设计,因为你很少需要知道这个。此外,“最顶层”的概念只在特定情况下有效,即使在那种情况下也不总是有效。 - Dave DeLong
@Daniel,我误读了您的问题。 尝试回答此问题时有很多情况需要考虑。这取决于您的视图控制器流程。 @Wilbur的答案应该是追踪它的好起点。 - Deepak Danduprolu
4
@Daniel:添加第二个UIWindow 对于警报视图类似的覆盖效果很有效。 - Wilbur Vandrsmith
是的,UIWindow似乎是实现警报的方法。这并不能解决找出最上层视图控制器(想要将其放在顶部的位置)的另一个问题,但我想这些问题需要分别处理。 - Hot Licks
显示剩余2条评论
42个回答

457

我认为你需要结合已经被接受的答案和 @fishstix 的答案。

+ (UIViewController*) topMostController
{
    UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;

    while (topController.presentedViewController) {
        topController = topController.presentedViewController;
    }

    return topController;
}

Swift 3.0+

func topMostController() -> UIViewController? {
    guard let window = UIApplication.shared.keyWindow, let rootViewController = window.rootViewController else {
        return nil
    }

    var topController = rootViewController

    while let newTopController = topController.presentedViewController {
        topController = newTopController
    }

    return topController
}

4
此外,您可以检查 UINavigationController 并请求其 topViewController,甚至可以检查 UITabBarController 并请求其 selectedViewController。这将获取当前对用户可见的视图控制器。 - Tricertops
39
这只是一个不完整的解决方案,因为它只遍历了以模态方式呈现的视图控制器的层次结构,而没有遍历 childViewControllers 的层次结构(这些由 UINavigationController、UITabBarController 等使用)。 - algal
3
这是一种很好的方式,可以将模态视图控制器的呈现抽象化,使其返回到当前应用程序状态。在我的情况下,这是应用程序超时后重新输入密码的屏幕。谢谢! - Eric
17
@algal: 不完全正确:UITabBarController和UINavigationController已经是层次结构中最顶层的视图控制器。根据您想要使用"最顶部的控制器"的目的,您可能根本不想遍历它们并操纵其内容。在我的情况下,我需要在所有内容之上呈现一个模态控制器,为此我需要获取UINavigationController或UITabBarController,而不是它们的内容! - Rick77
1
@LeMotJuiced,除非我写这段话时有所改变(我已经有一段时间没有编写iOs代码了),我的观察是正确的:UINavigationController和UITabControllers是“容器”,它们在逻辑上(以及在层次结构中)处于“顶部”。它们有自己的层次结构,但那是另一回事。 - Rick77
显示剩余9条评论

153
为了补充 JonasG 在遍历时忽略了选项卡栏控制器的答案(可以参考这里),以下是我返回当前可见视图控制器的版本:
- (UIViewController*)topViewController {
    return [self topViewControllerWithRootViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}

- (UIViewController*)topViewControllerWithRootViewController:(UIViewController*)rootViewController {
    if ([rootViewController isKindOfClass:[UITabBarController class]]) {
        UITabBarController* tabBarController = (UITabBarController*)rootViewController;
        return [self topViewControllerWithRootViewController:tabBarController.selectedViewController];
    } else if ([rootViewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController* navigationController = (UINavigationController*)rootViewController;
        return [self topViewControllerWithRootViewController:navigationController.visibleViewController];
    } else if (rootViewController.presentedViewController) {
        UIViewController* presentedViewController = rootViewController.presentedViewController;
        return [self topViewControllerWithRootViewController:presentedViewController];
    } else {
        return rootViewController;
    }
}

2
不错,是的,我忘记了选项卡控制器 :P - JonasG
9
不包括 childViewControllers - Awesome-o
请查看我下面的答案,它改进了上面的答案,处理了@kleo忽略的一些情况,例如弹出窗口、视图控制器添加为其他视图控制器的子视图时遍历。 - Rajesh
如果您正在使用return [self topViewControllerWithRootViewController:navigationController.visibleViewController];,则visibleViewController本身会返回显示的视图控制器(如果有的话),即使它是UIAlertController。对于需要避免UI警报控制器的人,请使用topViewController而不是visibleViewController - Johnykutty
如果 [rootViewController isKindOfClass:[UIAlertController class]],那么如何在 UIAlertController 下获取最顶层的视图控制器? - Steve Gear
显示剩余3条评论

79

iOS 4在UIWindow上引入了rootViewController属性:

[UIApplication sharedApplication].keyWindow.rootViewController;

不过,创建视图控制器后你需要自己设置它。


172
威尔伯,这会给你相反于原问题的结果。rootViewController是基础视图控制器而不是最顶层的控制器。 - m4rkk
3
"Top-most" 的含义取决于你的视角方向。新的控制器是按照类似栈的顺序从顶部添加还是类似树形结构的底部添加?无论如何,原帖提到导航控制器位于顶部,这意味着它采用自上而下的视图增长方式。 - Wilbur Vandrsmith
53
“Top”这个词用于视图控制器,它是指_visualy on the top_(例如-[UINavigationController topViewController])。然后有“root”这个词,它是树的根(例如-[UIWindow rootViewController])。 - Tricertops
13
我找不到这个东西的文档(documentation),Xcode 似乎也找不到。@ImpurestClub - Drux
5
不,它属于UINavigationController。 - David H
显示剩余6条评论

67
一个完整的非递归版本,考虑到不同的情况:
  • 视图控制器正在展示另一个视图
  • 视图控制器是一个UINavigationController
  • 视图控制器是一个UITabBarController

Objective-C

 UIViewController *topViewController = self.window.rootViewController;
 while (true)
 {
     if (topViewController.presentedViewController) {
         topViewController = topViewController.presentedViewController;
     } else if ([topViewController isKindOfClass:[UINavigationController class]]) {
         UINavigationController *nav = (UINavigationController *)topViewController;
         topViewController = nav.topViewController;
     } else if ([topViewController isKindOfClass:[UITabBarController class]]) {
         UITabBarController *tab = (UITabBarController *)topViewController;
         topViewController = tab.selectedViewController;
     } else {
         break;
     }
 }

Swift 4+

->

Swift 4+

extension UIWindow {
    func topViewController() -> UIViewController? {
        var top = self.rootViewController
        while true {
            if let presented = top?.presentedViewController {
                top = presented
            } else if let nav = top as? UINavigationController {
                top = nav.visibleViewController
            } else if let tab = top as? UITabBarController {
                top = tab.selectedViewController
            } else {
                break
            }
        }
        return top
    }
}

3
我将其命名为visibleViewController,以明确它的功能。 - Jonny
谢谢!我一整天都在苦苦寻找一个方法,让用户在应用程序在后台或前台时通过点击通知后能够呈现一个ViewController! - Shardon
2
当警告框正在显示时,我该如何避免使用UIAlertController? - famfamfam

32

使用扩展获取 Swift 中的顶层视图控制器

代码:

extension UIViewController {
    @objc func topMostViewController() -> UIViewController {
        // Handling Modal views
        if let presentedViewController = self.presentedViewController {
            return presentedViewController.topMostViewController()
        }
        // Handling UIViewController's added as subviews to some other views.
        else {
            for view in self.view.subviews
            {
                // Key property which most of us are unaware of / rarely use.
                if let subViewController = view.next {
                    if subViewController is UIViewController {
                        let viewController = subViewController as! UIViewController
                        return viewController.topMostViewController()
                    }
                }
            }
            return self
        }
    }
}

extension UITabBarController {
    override func topMostViewController() -> UIViewController {
        return self.selectedViewController!.topMostViewController()
    }
}

extension UINavigationController {
    override func topMostViewController() -> UIViewController {
        return self.visibleViewController!.topMostViewController()
    }
}

使用方法:

UIApplication.sharedApplication().keyWindow!.rootViewController!.topMostViewController()

非常好 - 非常感谢您提供的解决方案。子视图技巧是必需的!再次感谢,您救了我的一天。 - iKK

27

为了完善Eric的answer(他遗漏了弹出窗口、导航控制器、选项卡控制器、作为其他视图控制器子视图添加时遍历的视图控制器),这是我的版本,返回当前可见的视图控制器:

=====================================================================

- (UIViewController*)topViewController {
    return [self topViewControllerWithRootViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}

- (UIViewController*)topViewControllerWithRootViewController:(UIViewController*)viewController {
    if ([viewController isKindOfClass:[UITabBarController class]]) {
        UITabBarController* tabBarController = (UITabBarController*)viewController;
        return [self topViewControllerWithRootViewController:tabBarController.selectedViewController];
    } else if ([viewController isKindOfClass:[UINavigationController class]]) {
        UINavigationController* navContObj = (UINavigationController*)viewController;
        return [self topViewControllerWithRootViewController:navContObj.visibleViewController];
    } else if (viewController.presentedViewController && !viewController.presentedViewController.isBeingDismissed) {
        UIViewController* presentedViewController = viewController.presentedViewController;
        return [self topViewControllerWithRootViewController:presentedViewController];
    }
    else {
        for (UIView *view in [viewController.view subviews])
        {
            id subViewController = [view nextResponder];
            if ( subViewController && [subViewController isKindOfClass:[UIViewController class]])
            {
                if ([(UIViewController *)subViewController presentedViewController]  && ![subViewController presentedViewController].isBeingDismissed) {
                    return [self topViewControllerWithRootViewController:[(UIViewController *)subViewController presentedViewController]];
                }
            }
        }
        return viewController;
    }
}

=====================================================================

现在,要获取最顶层的视图控制器,您只需要按照上述方法调用以下方法即可:
UIViewController *topMostViewControllerObj = [self topViewController];

还缺少SplitViewController吗? - apinho

21

这个答案包括childViewControllers,并保持了一个干净易读的实现。

+ (UIViewController *)topViewController
{
    UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;

    return [rootViewController topVisibleViewController];
}

- (UIViewController *)topVisibleViewController
{
    if ([self isKindOfClass:[UITabBarController class]])
    {
        UITabBarController *tabBarController = (UITabBarController *)self;
        return [tabBarController.selectedViewController topVisibleViewController];
    }
    else if ([self isKindOfClass:[UINavigationController class]])
    {
        UINavigationController *navigationController = (UINavigationController *)self;
        return [navigationController.visibleViewController topVisibleViewController];
    }
    else if (self.presentedViewController)
    {
        return [self.presentedViewController topVisibleViewController];
    }
    else if (self.childViewControllers.count > 0)
    {
        return [self.childViewControllers.lastObject topVisibleViewController];
    }

    return self;
}

更新了一些代码,通过最小化并恢复它来显示控制器是什么。http://nik-kov-ios-developer.blogspot.ru/2016/12/find-out-what-controller-is-on-top.html - Nike Kov
嘿,快点,你的“topVisibleViewController”在哪里? - Paradise

11

最新的Swift版本:
创建一个文件,命名为UIWindowExtension.swift,并粘贴以下代码片段:

import UIKit

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
            }
        }
    }
}

func getTopViewController() -> UIViewController? {
    let appDelegate = UIApplication.shared.delegate
    if let window = appDelegate!.window {
        return window?.visibleViewController
    }
    return nil
}

可在任何地方使用,例如:

if let topVC = getTopViewController() {

}

我不想太大改你的答案,但建议几点。1. 添加UISplitViewController支持。2. 使用switch代替if else。3. 不确定是否需要静态函数,我认为你可以在第一个实例级变量中轻松完成此操作。4. 最好不要创建太多全局函数,但这是品味问题。您可以使用一行代码来实现全局函数的效果:UIApplication.sharedApplication().delegate?.window?.visibleViewController - Jordan Smith

11
我最近遇到了这样一个情况,我的项目需要在网络状态改变时显示一个通知视图,无论控制器显示的是什么类型(UINavigationController、经典控制器或自定义视图控制器)。所以我刚刚发布了我的代码。它非常简单,实际上基于一种协议,因此可以适用于各种类型的容器控制器。看起来与之前的答案有关,但方式更加灵活。你可以在这里获取代码:PPTopMostController 并使用以下方法获取最顶层的控制器:
UIViewController *c = [UIViewController topMostController];

10

使用以下扩展来获取当前可见的UIViewController。适用于Swift 4.0及更高版本。

Swift 4.0及更高版本:

extension UIApplication {
    
    class func topViewController(_ viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let nav = viewController as? UINavigationController {
            return topViewController(nav.visibleViewController)
        }
        if let tab = viewController as? UITabBarController {
            if let selected = tab.selectedViewController {
                return topViewController(selected)
            }
        }
        if let presented = viewController?.presentedViewController {
            return topViewController(presented)
        }
        return viewController
    }
}

如何使用?

let objViewcontroller = UIApplication.topViewController()

在检测UINavigationControllerUITabBarController的情况之前,难道不应该首先测试presentedViewController吗?否则,如果一个视图控制器是从UINavigationControllerUITabBarController模态呈现的,即使它是可见的视图控制器,它也不会被返回为顶部视图控制器。 - Drew

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