iOS開發(fā)過程中,多人開發(fā)或者導入第三方框架的時候,可能碰到UIWindow層級沖突的問題。
例如,很多人習慣在keyWindow上添加一個自定義浮層視圖,但是,當自己或者其它第三方框架曾經調高過其它自定義UIWindow屬性windowLevel,或者有其它同級windowLevel的UIWindow后來改變過顯示狀態(tài)(如.hidden=NO,makeKeyAndVisible等),而且又沒有 設將其設置為keyWindow,結果導致正在顯示的UIWindow不是keyWindow,從而導致添加到keyWindow上自定義視圖無法顯示(被覆蓋了)。
一. 為App初始化一個默認UIWindow對象
在AppDelegate.m中需要初始化一個window屬性,作為后面往App添加視圖的容器
1. 初始化操作寫在如下UIApplicationDelegate代理方法中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
2. 一個初始化window操作示例如下,具體根據產品需求設置
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
3. 接下來需指定window的rootViewController
//判斷該用首次歡迎頁or密碼登錄頁等等
if ([CommonUtils isStringNilOrEmpty:[CommonUtils getStrValueInUDWithKey:kIsGuide]]) {
[CommonUtils saveStrValueInUD:@"1" forKey:kGuide];
// 歡迎頁置為rootViewController
self.window.rootViewController = [[WHGuidePagesController alloc]initWithNibName:@"WHGuidePagesController" bundle:nil];
}else{
// 檢查各種密碼決定登錄方式,并分別設置rootViewController
[self chooseVerifyMethod];
}
4. 注意點:rootViewController屬性
- 目前只有UIWindow有rootViewController這個屬性,不要跟UINavigationController里面的根視圖概念混淆。
- UINavigationController其實并沒有 rootViewController這個屬性!也就沒有自帶的setter方法。要設置其根視圖只能通過如下方法,而不能通過屬性的setter方法和點語法設置根視圖。
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController;
// Convenience method pushes the root view controller without animation.
5. 大多數APP的視圖層級關系(以有底部TabBar的App為例)
- 1). [UIApplication sharedApplication].keyWindow為UIWindow對象。比如,獲取APP的keyWindow并往上添加視圖的代碼:
[[UIApplication sharedApplication].keyWindow addSubview:self.signView];
- 2). 假設APP的keyWindow對象為uiWindow,則uiWindow.rootViewController為UITabBarController對象(也只有UIWindow可以用點語法設置根視圖)。比如,為設置rootViewController代碼:
self.window.rootViewController = customTabBarVC;//AppDelegate.m里面
- 3). UITabBarController對象的viewControllers包含UINavigationController對象。設置其viewControllers的方法:
- (void)setViewControllers:(NSArray<__kindof UIViewController *> * __nullable)viewControllers animated:(BOOL)animated;
- 4). UINavigationController對象的rootViewController為UIViewController對象。初始化其rootViewController的方法為:
- (instancetype)initWithRootViewController:(UIViewController *)rootViewController;
6. 獲取keyWindow(它并不一定是當前最上層顯示的window)的rootViewController
可以通過如下方法找到當前UIWindow的rootViewController,前提是當keyWindow真的顯示在最上層。
#pragma mark - 獲取根視圖的(導航、標簽)視圖控制器
+ (UINavigationController *)getRootVCformViewController
{
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UINavigationController *nav = nil;
if ([rootVC isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabbar = (UITabBarController *)rootVC;
NSInteger index = tabbar.selectedIndex;
nav = tabbar.childViewControllers[index];
}else if ([rootVC isKindOfClass:[UINavigationController class]]) {
nav = (UINavigationController *)rootVC;
}else if ([rootVC isKindOfClass:[UIViewController class]]) {
NSLog(@"This no UINavigationController...");
}
return nav;
}
二. 在自定義的UIWindow添加自定義視圖
假設想為一個APP添加一個手勢驗證的頁面,當進入APP彈出這個手勢驗證頁面。如果不想影響原來的UIWindow,可以考慮新建一個UIWindow并覆蓋原來的UIWindow,并往新建的UIWindow上添加各種手勢相關的視圖及控制器。但在手勢驗證完后,務必銷毀這個自定義的UIWindow,否則可能導致看不見的UIWindow越積越多。
1. 自定義UIWindow
_window = [[UIWindow alloc]initWithFrame:[[UIScreen mainScreen] bounds]];
_window.hidden = NO;
[self.window makeKeyAndVisible];
2. 指定自定義視圖控制器
UIViewController *vc = [[UIViewController alloc]init];
_window.rootViewController = vc;
3. 銷毀自定義UIWindow
自定義視圖用完后,記得要銷毀自定義的UIWindow,否則導致APP以后會有越來越多沒用到的UIWindow,即使再也沒有顯示過它們,但是可以用調試工具看到許多廢棄的window。可參考方法如下
- (void)dismiss {
[self.window resignKeyWindow];
self.window.windowLevel = -1000;
self.window.hidden = YES;
[self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
self.window = nil;
}
三. UIWindow的顯示特性
1. 相同windowLevel下,調整UIWindow顯示層的基本方法
1). 顯示相關屬性:hidden
- 如果僅僅想顯示一個UIWindow
customWindow.hidden = NO;
PS: 雖然設置自己的hidden即可顯示出來,但上述方法并不會"自動"影響之前顯示的UIWindow對象的hidden屬性。如果,之前UIWindow的hidden = NO,設置新UIWindow的hidden將舊UIWindow覆蓋后,舊UIWindow的hidden屬性依舊為NO。
- 如果僅僅想隱藏一個UIWindow
customWindow.hidden = YES;
PS: 如果你沒有專門設置過hidden屬性,系統(tǒng)默認為YES。上述代碼會將UIWindow絕對隱藏,不管有沒其他UIWindow覆蓋。當也沒有其它非隱藏的UIWindow的時候,APP屏幕完全黑屏。
- 如果想顯示一個UIWindow,同時設置為keyWindow,并將其顯示在同一windowLevel的其它任何UIWindow之上
- (void)makeKeyAndVisible
PS: 上述方法真的會將其顯示在同一windowLevel的其它任何UIWindow之上!顯示最上層的UIWindow以最后執(zhí)行過該代碼的UIWindow為準。
2). 顯示相關方法:makeKeyAndVisible的作用
[self.window makeKeyAndVisible];
其執(zhí)行效果包括 但不限于 執(zhí)行了如下代碼(因為還會覆蓋同level的所有window):
[self.window makeKeyWindow];
self.window.hidden = NO;
講真,makeKeyAndVisible真的會自動改變hidden屬性值為NO。
3). UIWindow對象的hidden屬性默認值
- 默認值:YES
PS:如果你僅僅創(chuàng)建一個UIWindow,而又不專門設置hidden屬性(或者makeKeyAndVisible),系統(tǒng)默認分配的默認值為true。例如,我們把影響到hidden屬性的方法屏蔽掉:
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
// [self.window makeKeyAndVisible];
// [self.window makeKeyWindow];
// self.window.hidden = NO;
再來打印hidden屬性如下:
po self.window.isHidden
true
4). 誤區(qū):關于keyWindow的混淆易錯點
設置keyWindow與否并不 影響視圖層級顯示,僅來接收鍵盤及其它非觸摸事件。如果沒有專門設置過keyWindow的hiden為NO,而且也沒有其它非隱藏的UIWindow,那么APP會黑屏。
- 如果僅僅設置為keyWindow
- (void)makeKeyWindow
- 如果僅僅解除為keyWindow
- (void)resignKeyWindow
app的keyWindow與是否在最上層顯示沒有任何關系。比如,你如果想通過
[[UIApplication sharedApplication] keyWindow]
獲取正在顯示的UIWindow是極其不準確 的。有時候通過這個代碼獲取的如果真的是正在顯示的UIWindow,僅僅是因為碰巧而已。
5). 警惕點:有多個hidden屬性=NO的UIWindow,該顯示誰?
如上所見,makeKeyAndVisible與hidden的setter方法均可以改變hidden的值,但有個問題,經過多次調整,可能有多個UIWindow的hidden都為NO,那么應該顯示誰?
- 對于hidden的setter方法,最終顯示的以最后 執(zhí)行過 .hidden=NO 的UIWindow為準,且執(zhí)行 .hidden=NO 之前hidden的值為YES。(hidden如果是從NO改為NO的不 算 最后 改變UIWindow的顯示狀態(tài))
- 對于makeKeyAndVisible方法,最終顯示的以最后 執(zhí)行過 makeKeyAndVisible 的UIWindow為準。
- 對于先后分別用makeKeyAndVisible方法和hidden的setter方法,還是先后分別用hidden的setter方法和makeKeyAndVisible方法,結局同樣以最后改變顯示狀態(tài)的UIWindow為準。
2. 基于windowLevel,調整UIWindow顯示層的拓展方法
先去UIWindow.h里面看看UIWindowLevel的定義:
typedef CGFloat UIWindowLevel;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar __TVOS_PROHIBITED;
例如,在手勢相關類中調整自定義的UIWindow層級
[self.window makeKeyAndVisible];
_window.windowLevel = UIWindowLevelAlert;
- 打印代表UIWindowLevelAlert層級的數據值
(lldb) po self.window.windowLevel
2000
- 同理,打印代表UIWindowLevelStatusBar層級的數據值
(lldb) po self.window.windowLevel
1000
- 同理,打印代表UIWindowLevelNormal層級的數據值
(lldb) po self.window.windowLevel
0
小結:
- windowLevel數值越大的顯示在窗口棧的越上面
- 顯示層的優(yōu)先級 為: UIWindowLevelAlert > UIWindowLevelStatusBar > UIWindowLevelNormal
- 系統(tǒng)給UIWindow默認的windowLevel為UIWindowLevelNormal
四. UIWindow常見操作方法總結
1. 獲取App所有window的windows數組
[[UIApplication sharedApplication] windows]
例如,第三方加載動畫框架KVNProcess中KVNProgress.m文件會有一段這樣的代碼:
- (void)addToCurrentWindow
{
UIWindow *currentWindow = nil;
NSEnumerator *frontToBackWindows = [[[UIApplication sharedApplication] windows] reverseObjectEnumerator];
for (UIWindow *window in frontToBackWindows) {
if (window.windowLevel == UIWindowLevelNormal) {
currentWindow = window;
break;
}
}
if (self.superview != currentWindow) {
[self addToView:currentWindow];
}
}
2. keyWindow
[[UIApplication sharedApplication] keyWindow]
例如,第三方下拉菜單框架FFDropDownMenu的FFDropDownMenuView.m文件中有這樣一段代碼:
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[keyWindow addSubview:self];
這段代碼的目的是添加到最上層UIWindow,但實際操作是把自己的視圖添加到keyWindow上。其實,如果我們在編寫代碼時嚴謹地保證keyWindow是顯示在最上層的UIWindow,這樣寫沒有問題。但如果:自己或者其它第三方框架曾經調高過其它UIWindow屬性windowLevel,或者有同級windowLevel的其它UIWindow后來改變過顯示狀態(tài)(如.hidden=NO,makeKeyAndVisible等),可能會導致下拉菜單的彈出視圖無法顯示(被覆蓋)。
3. 獲取AppDelegate單例的window屬性
專門獲取AppDelegate.m文件中的window屬性,不包含其它其定義的window
[[[UIApplication sharedApplication] delegate] window]
拓展一下,獲取AppDelegate單例的方法為
+ (AppDelegate *)sharedDelegate
{
return (AppDelegate *)[[UIApplication sharedApplication] delegate];
}
附. 調試打印例子
- 啟動APP,AppDelegate.m中的window屬性
(lldb) po self.window
<UIWindow: 0x15fd24390; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1700567a0>; layer = <UIWindowLayer: 0x170233700>>
- 跳轉手勢,GestureScreen.m中的window屬性
(lldb) po _window
<UIWindow: 0x15fd29160; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x170057f70>; layer = <UIWindowLayer: 0x1702345c0>>
- 此時,可查看所有window
(lldb) po [[UIApplication sharedApplication] windows]
<__NSArrayM 0x17405c290>(
<UIWindow: 0x15fd24390; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1700567a0>; layer = <UIWindowLayer: 0x170233700>>,
<UIWindow: 0x15fd29160; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x170057f70>; layer = <UIWindowLayer: 0x1702345c0>>
)
- 此時,斷點在手勢相關類中,也可專門查看AppDelegate.m中的window屬性:假設
UIWindow *delegateWindow = [[[UIApplication sharedApplication] delegate] window];
打印如下
(lldb) po delegateWindow
<UIWindow: 0x15fd24390; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1700567a0>; layer = <UIWindowLayer: 0x170233700>>