获取当前屏幕显示的视图控制器
有个需求,就是在无论任何环境下,没有任何参数,我都要获得当前屏幕所显示的视图控制器。开始尝试。
一.
有问题问Google,摸索阶段得到了这样的一段代码,时间大约是2016年五月:
1 //获取当前屏幕显示的viewcontroller 2 - (UIViewController *)getCurrentVC { 3 4 UIViewController *result = nil; 5 6 UIWindow * window = [[UIApplication sharedApplication] keyWindow]; 7 if (window.windowLevel != UIWindowLevelNormal) { 8 9 NSArray *windows = [[UIApplication sharedApplication] windows]; 10 for(UIWindow * tmpWin in windows) { 11 12 if (tmpWin.windowLevel == UIWindowLevelNormal) { 13 14 window = tmpWin; 15 break; 16 } 17 } 18 } 19 20 UIView *frontView = [[window subviews] objectAtIndex:0]; 21 id nextResponder = [frontView nextResponder]; 22 23 if ([nextResponder isKindOfClass:[UIViewController class]]) 24 result = nextResponder; 25 else 26 result = window.rootViewController; 27 28 return result; 29 }
这段代码由两个部分组成。我们项目是Swift语言为主体,我就尝试用Swift重写一下这个部分的代码:
1.获得当前显示的window:
1 // 找到当前显示的window 2 class func getCurrentWindow() -> UIWindow? { 3 4 // 找到当前显示的UIWindow 5 var window: UIWindow? = UIApplication.shared.keyWindow 6 /** 7 window有一个属性:windowLevel 8 当 windowLevel == UIWindowLevelNormal 的时候,表示这个window是当前屏幕正在显示的window 9 */ 10 if window?.windowLevel != UIWindowLevelNormal { 11 12 for tempWindow in UIApplication.shared.windows { 13 14 if tempWindow.windowLevel == UIWindowLevelNormal { 15 16 window = tempWindow 17 break 18 } 19 } 20 } 21 22 return window 23 }
注释写的比较清楚了。如果你们的项目只有一个window,那么可以直接获得keyWindow就可以了;但是我建议还是判断一下,保险。
2.获得当前的视图控制器:
1 // MARK: 获取当前屏幕显示的viewController 2 class func getCurrentViewController1() -> UIViewController? { 3 4 // 1.声明UIViewController类型的指针 5 var viewController: UIViewController? 6 7 // 2.找到当前显示的UIWindow 8 let window: UIWindow? = self.getCurrentWindow() 9 10 // 3.获得当前显示的UIWindow展示在最上面的view 11 let frontView = window?.subviews.first 12 13 // 4.找到这个view的nextResponder 14 let nextResponder = frontView?.next 15 16 if nextResponder?.isKind(of: UIViewController.classForCoder()) == true { 17 18 viewController = nextResponder as? UIViewController 19 } 20 else { 21 22 viewController = window?.rootViewController 23 } 24 25 return viewController 26 }
这两个方法一起使用。
我先说结论:不好用。始终返回的都是 window的rootViewController。问题出在哪里?经过我的一路艰苦的判断,最终确定问题出在 let frontView = window?.subviews.first 这一句上。
我不多说,我打印了下 window?.subviews 这个数组以及 nextResponder 这个UIResponder。结果如下:
Optional([<UITransitionView: 0x1025f6090; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x174430a60>>, <UITransitionView: 0x102585880; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x174434740>>]) Optional(<UIWindow: 0x102518a30; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x174256d10>; layer = <UIWindowLayer: 0x17403d5a0>>)
可以看出,不管是subviews中的第一个还是第二个UIView,都是 UITransitionView 这样一个类型。这个类型是window与实际显示的view之间的过渡层,它的 nextResponder 直接就是window了。也就是说,这个view根本就不是当前显示在屏幕最上面的view。这条线索随之中断。
二.
又继续查询,发现大家的博文写的都差不多,几乎都是上面最初的那段代码,一模一样。不过我终于还是找到这样一条博文:http://www.jianshu.com/p/cb855c0f12ca,与我遇到了几乎一样的问题;而他的解决方式就是沿着响应链继续向上寻找。而在评论区有更好的一个思路:对UIView创建分类,你给我一个view,我就还你控制这个view的第一个视图控制器:http://00red.com/blog/2015/05/23/tips-find-controller/。
我直接粘贴代码了:
1 extension UIView { 2 3 func findController() -> UIViewController! { 4 5 return self.findControllerWithClass(clzz: UIViewController.self) 6 } 7 8 func findNavigator() -> UINavigationController! { 9 10 return self.findControllerWithClass(clzz: UINavigationController.self) 11 } 12 13 func findControllerWithClass<T>(clzz: AnyClass) -> T? { 14 15 var responder = self.next 16 17 while(responder != nil) { 18 19 if (responder?.isKind(of: clzz))! { 20 21 return responder as? T 22 } 23 responder = responder?.next 24 } 25 26 return nil 27 } 28 }
这个很明显,我们只需要拿到frontView之后,这样沿着响应链循环查找一下,应该就有结果了:
1 viewController = frontView?.findController()
非常抱歉,又失败了。返回值为空。
为此,我专门打印了一下在while循环中所有的responder的值:
1 Optional(<UIWindow: 0x102518a30; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x174256d10>; layer = <UIWindowLayer: 0x17403d5a0>>) 2 Optional(<UIApplication: 0x1024055f0>) 3 Optional(<App.AppDelegate: 0x17413ef00>)
恩,三步就出了App了。也就是说,这条响应链中根本就找不到UIViewController的身影。
应该说,这个分类的思路是正确的,但是这个方法需要一个参数,就是一个view;它本质上是找到持有这个view的视图控制器;但是开头就说了,我们没有这样的一个view,没有任何参数。所以分类很好,但不适用我们这个问题。
三.
好吧,看来通过window.subviews这个方法,似乎是行不通的。那么真的没有办法获得视图控制器了吗?
其实办法还是有的。
我们知道,一般我们的架构都是这样的:
UIApplication -> UIWindow -> UITabBarController -> UINavigationController -> push/present出来的视图控制器
我们要拿到的也就是第五层。什么办法呢?
自然要请出我们的递归算法咯~
1 /* 递归找最上面的viewController */ 2 @objc class func topViewController() -> UIViewController? { 3 4 return self.topViewControllerWithRootViewController(viewController: self.getCurrentWindow()?.rootViewController) 5 } 6 7 @objc class func topViewControllerWithRootViewController(viewController :UIViewController?) -> UIViewController? { 8 9 if viewController == nil { 10 11 return nil 12 } 13 14 if viewController?.presentedViewController != nil { 15 16 return self.topViewControllerWithRootViewController(viewController: viewController?.presentedViewController!) 17 } 18 else if viewController?.isKind(of: UITabBarController.self) == true { 19 20 return self.topViewControllerWithRootViewController(viewController: (viewController as! UITabBarController).selectedViewController) 21 } 22 else if viewController?.isKind(of: UINavigationController.self) == true { 23 24 return self.topViewControllerWithRootViewController(viewController: (viewController as! UINavigationController).visibleViewController) 25 } 26 else { 27 28 return viewController 29 } 30 }
从头上一路向下找,最终找到当前显示的控制器。简单粗暴,但其实处处是美感,这正是递归的魅力啊。
最终调用 topViewController() 即可。