有個需求,就是在無論任何環境下,沒有任何參數,我都要獲得當前屏幕所顯示的視圖控制器。開始嘗試。
一.
有問題問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() 即可。