在AppDelegate.m中获取当前显示的UIViewController

137

如何在AppDelegate.mapplication:didReceiveRemoteNotification方法中获取当前屏幕上的UIViewController,以便响应来自APNs的推送通知并设置一些徽章视图?

我尝试使用self.window.rootViewController获取当前显示的UIViewController,它可能是一个UINavigationViewController或其他类型的视图控制器。我发现可以使用UINavigationViewControllervisibleViewController属性获取屏幕上的UIViewController,但如果它不是一个UINavigationViewController怎么办?

非常感谢您的帮助!以下是相关代码。

AppDelegate.m

...
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {

    //I would like to find out which view controller is on the screen here.

    UIViewController *vc = [(UINavigationViewController *)self.window.rootViewController visibleViewController];
    [vc performSelector:@selector(handleThePushNotification:) withObject:userInfo];
}
...

ViewControllerA.m

- (void)handleThePushNotification:(NSDictionary *)userInfo{

    //set some badge view here

}
20个回答

111

我总是喜欢涉及类别的解决方案,因为它们可以很容易地添加并且可以轻松地重用。

所以我在UIWindow上创建了一个类别。您现在可以在UIWindow上调用visibleViewController,通过搜索控制器层次结构来获取可见视图控制器。如果您正在使用导航和/或选项卡栏控制器,则此方法有效。如果您有其他类型的控制器建议,请告诉我,我可以添加它。

UIWindow+PazLabs.h(头文件)

#import <UIKit/UIKit.h>

@interface UIWindow (PazLabs)

- (UIViewController *) visibleViewController;

@end

UIWindow+PazLabs.m(实现文件)

#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 版本

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

2
我该如何在Swift版本中使用这个? - Vijay Singh Rana
2
我无法理解你的问题。请将其复制并粘贴到您的代码中。 - zirinisp
自定义容器VC怎么样? - Mingming
@Mingming,增加一个额外的if来检查是否为自定义容器VC(在getVisibielController方法中)应该不难,如果是,则返回“visible”控制器,对于大多数自定义容器VC实现(我想),这通常是vc.childControllers.lastObject,但这取决于它的实现方式。 - gadu
我在继承的项目中遇到了这个代码块,发现AlertController也是层次结构的一部分。你不能在另一个AlertController之上呈现AlertController,因为它不是常规VC。 - Lucas van Dongen
1
我刚刚发布了一个与此答案相同的方法,只是更新了语法:它使用了switch-case并遵循Swift 3的命名约定:https://dev59.com/dGgu5IYBdhLWcg3wByq1#42486823 - Jeehut

104

即使您的控制器不是UINavigationController,您也可以使用rootViewController

UIViewController *vc = self.window.rootViewController;

一旦您知道了根视图控制器,那么取决于您如何构建您的用户界面,但您可能可以找到一种浏览控制器层次结构的方法。

如果您提供有关应用程序定义方式的更多详细信息,我可能会给出更多提示。

编辑:

如果您想要顶部的 视图(而不是视图控制器),您可以检查

[[[[UIApplication sharedApplication] keyWindow] subviews] lastObject];

尽管这个视图可能是不可见的,甚至被它的一些子视图覆盖......

再次强调,这取决于你的用户界面,但这可能会有所帮助......


21
问题在于,如果可见视图不属于根视图控制器(例如模态视图等情况),就会出现这种情况。 - Dima
3
你看,UINavigationController 提供了一种知道顶层控制器是哪个的方法;你的根控制器也应该以某种方式提供相同的信息。通常情况下无法推断它,因为它严格依赖于你如何构建你的用户界面,并且没有明确的控制器层次结构(就像视图那样)。你可以简单地向根控制器添加一个属性,并在每次“push”新控制器时设置其值。 - sergio
1
只要值保持最新,这对我来说似乎是一个不错的选择。 - Dima
4
UIView 实例没有直接访问控制器的方式。rootViewController 不一定是当前显示的控制器,它只是视图层次结构的顶部。 - Gingi
如果您想从任何UIViewController中获取内容而不仅仅是从rootViewController中获取,请查看此答案:https://dev59.com/nmkw5IYBdhLWcg3wQ4Zc#29773513 - Arben Pnishi
显示剩余5条评论

49

Swift中用于UIApplication的简单扩展 (在iPhone上即使涉及UITabBarController内的moreNavigationController也能正常工作):

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

简单使用:

    if let rootViewController = UIApplication.topViewController() {
        //do sth with root view controller
    }

完美工作:-)

更新以获得清洁的代码:

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

1
这似乎是Swift 2.x的代码。Swift 3.x不再有“where”。此外,“sharedApplication()”现在是“shared”。这不是什么大问题,只需要花一分钟更新即可。可能需要提到它使用递归。另外,每次调用topViewController都应该需要“base:”前缀。 - Jeff Muir

38
您还可以通过NSNotificationCenter发布通知。这使您能够处理许多情况,其中遍历视图控制器层次结构可能会很棘手 - 例如在显示模态视图时等。
例如,
// 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];
}

在每个视图控制器中:

-(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 https://dev59.com/GXE85IYBdhLWcg3wbTD1#2777460
   if (self.isViewLoaded && self.view.window) {
      // handle the notification
   }
}

您还可以使用这种方法来对需要在接收到通知时更新并由多个视图控制器使用的控件进行检测。在这种情况下,分别在init和dealloc方法中处理添加/删除观察者的调用。


1
viewDidLoad 内的 addObserver:bar 是什么?我需要用 self 替换吗? - CainaSouza
谢谢指出,应该是self。我会更新答案的。 - Aneil Mallavarapu
获取userInfo中的所有键时崩溃...有什么想法吗?[NSConcreteNotification allKeys]:向实例0x1fd87480发送未识别的选择器 2013-07-05 16:10:36.469 Providence[2961:907] ***由于未捕获的异常'NSInvalidArgumentException'而终止应用程序,原因:'-[NSConcreteNotification allKeys]:向实例0x1fd87480发送未识别的选择器' - Awais Tariq
@AwaisTariq - 嗯 - 我猜iOS传递给didReceiveRemoteNotification的对象实际上不是NSDictionary,正如接口所指定的那样。 - Aneil Mallavarapu
如果用户尚未导航到您的观察者类呢? :/ - halbano
@halbano - 如果观察者尚未加载,那应该没关系吧?只需在创建时使用新数据来初始化它即可。 - Aneil Mallavarapu

25

代码

以下是使用Swift 3/4/5中伟大的switch-case语法的方法:

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

基本思路与zirinisp的答案相同,只是使用更类似于Swift 3+的语法。


用法

您可能想要创建一个名为UIWindowExt.swift的文件,并复制上面的扩展代码到其中。

在调用方面,它可以不使用任何特定的视图控制器来使用:

if let visibleViewCtrl = UIApplication.shared.keyWindow?.visibleViewController {
    // do whatever you want with your `visibleViewCtrl`
}

或者,如果您知道可见的视图控制器可以从特定的视图控制器到达:

if let visibleViewCtrl = UIWindow.visibleViewController(from: specificViewCtrl) {
    // do whatever you want with your `visibleViewCtrl`
}

希望它有所帮助!


第三种情况会因为无限递归而崩溃。修复方法是将vc重命名为presentingViewController,并将presentingViewController.presentedViewController作为参数传递给递归方法。 - Ikhsan Assaat
抱歉,我没有完全理解。您的意思是 UIWindow.visibleViewController(from: presentedViewController) 应该改为 UIWindow.visibleViewController(from: presentingViewController.presentedViewController) 吗? - Jeehut
正确的是,presentedViewControllerviewController是同一个对象,并且它将调用自身的方法,直到堆栈溢出(意在双关)。因此,它将是return UIWindow.visibleViewController(from: presentingViewController.presentedViewController)``` - Ikhsan Assaat
2
这个解决方案在其他方案无效时起作用了。你应该升级到Swift 5。基本上没有变化,只需更新答案的头文件即可。 - TM Lynch

14

比其他解决方案少得多的代码:

Objective-C 版本:

- (UIViewController *)getTopViewController {
    UIViewController *topViewController = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (topViewController.presentedViewController) topViewController = topViewController.presentedViewController;

    return topViewController;
}

Swift 2.0版本:(感谢Steve.B)

func getTopViewController() -> UIViewController {
    var topViewController = UIApplication.sharedApplication().delegate!.window!!.rootViewController!
    while (topViewController.presentedViewController != nil) {
        topViewController = topViewController.presentedViewController!
    }
    return topViewController
}

可在您的应用程序中任何地方使用,包括模态框。


1
这种情况下,它无法处理呈现的视图控制器是一个具有自己子级的“UINavigationController”的情况。 - levigroker
@levigroker,也许是你的视图架构方式有问题?对我来说,使用这个和导航一起使用很好。(这就是我使用它的方式) - jungledev
@jungledev 我相信你是正确的。话虽如此,我们需要的是适用于所有视图控制器配置的解决方案。 - levigroker
@levigroker 它在所有标准的vc配置中都可以工作 - 我所工作的应用程序具有非常复杂的架构,被超过500k用户使用,并且在应用程序的任何地方都可以正常工作。也许你应该发布一个问题,附带代码示例,询问为什么它在你的视图中无法工作? - jungledev
jungledev:我很高兴这段代码对你有用,但它似乎不是一个完整的解决方案。@zirinisp的回答在我的情况下完美地解决了问题。 - levigroker
显示剩余6条评论

14

我发现iOS 8把一切都搞砸了。在iOS 7中,每当你有一个以模态方式呈现的UINavigationController时,视图层次结构中就会出现一个新的UITransitionView。以下是我的代码,它能找到最顶层的VC。调用getTopMostViewController应该返回一个VC,你可以向它发送像presentViewController:animated:completion这样的消息。它的目的是为您提供一个VC,您可以使用它来呈现模态VC,因此它最有可能停止并返回容器类(如UINavigationController),而不是其中包含的VC。很容易将代码改成适应该要求。我已经在iOS 6、7和8的各种情况下测试过这段代码。如果你发现了错误,请告诉我。

+ (UIViewController*) getTopMostViewController
{
    UIWindow *window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal) {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(window in windows) {
            if (window.windowLevel == UIWindowLevelNormal) {
                break;
            }
        }
    }

    for (UIView *subView in [window subviews])
    {
        UIResponder *responder = [subView nextResponder];

        //added this block of code for iOS 8 which puts a UITransitionView in between the UIWindow and the UILayoutContainerView
        if ([responder isEqual:window])
        {
            //this is a UITransitionView
            if ([[subView subviews] count])
            {
                UIView *subSubView = [subView subviews][0]; //this should be the UILayoutContainerView
                responder = [subSubView nextResponder];
            }
        }

        if([responder isKindOfClass:[UIViewController class]]) {
            return [self topViewController: (UIViewController *) responder];
        }
    }

    return nil;
}

+ (UIViewController *) topViewController: (UIViewController *) controller
{
    BOOL isPresenting = NO;
    do {
        // this path is called only on iOS 6+, so -presentedViewController is fine here.
        UIViewController *presented = [controller presentedViewController];
        isPresenting = presented != nil;
        if(presented != nil) {
            controller = presented;
        }

    } while (isPresenting);

    return controller;
}

请勿重复回答 - 如果问题是重复的,请将其标记为重复;如果不是重复的,请给予它们应得的具体回答。 - Flexo

8

zirinisp在Swift中的回答:

extension UIWindow {

    func visibleViewController() -> UIViewController? {
        if let rootViewController: UIViewController  = self.rootViewController {
            return UIWindow.getVisibleViewControllerFrom(rootViewController)
        }
        return nil
    }

    class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {

        if vc.isKindOfClass(UINavigationController.self) {

            let navigationController = vc as UINavigationController
            return UIWindow.getVisibleViewControllerFrom( navigationController.visibleViewController)

        } else if vc.isKindOfClass(UITabBarController.self) {

            let tabBarController = vc as UITabBarController
            return UIWindow.getVisibleViewControllerFrom(tabBarController.selectedViewController!)

        } else {

            if let presentedViewController = vc.presentedViewController {

                return UIWindow.getVisibleViewControllerFrom(presentedViewController.presentedViewController!)

            } else {

                return vc;
            }
        }
    }
}

使用方法:

 if let topController = window.visibleViewController() {
            println(topController)
        }

这是Swift 2.0中的as!navigationController.visibleViewController! - LinusGeffarth

7

为每个ViewController指定标题,然后通过下面给出的代码获取当前ViewController的标题。

-(void)viewDidUnload {
  NSString *currentController = self.navigationController.visibleViewController.title;

然后像这样通过您的标题检查它。
  if([currentController isEqualToString:@"myViewControllerTitle"]){
    //write your code according to View controller.
  }
}

毫无疑问,这是最好的答案。你也可以使用以下代码为你的视图控制器命名: self.title = myPhotoView - Resty

5
我的更好! :)
extension UIApplication {
    var visibleViewController : UIViewController? {
        return keyWindow?.rootViewController?.topViewController
    }
}

extension UIViewController {
    fileprivate var topViewController: UIViewController {
        switch self {
        case is UINavigationController:
            return (self as! UINavigationController).visibleViewController?.topViewController ?? self
        case is UITabBarController:
            return (self as! UITabBarController).selectedViewController?.topViewController ?? self
        default:
            return presentedViewController?.topViewController ?? self
        }
    }
}

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