UIWindow是一个继承自UIUIView的特殊类,但是同时它又具备了许多UIView不具备的特点,例如UIWindow对象不用通过添加到父试图就可以进行显示等.今天我们来聊一下UIWindow这个对象.
1. UIWindow简介
UIWindow是我们开发中常用的对象类型,用于管理、协调应用中显示的窗口,继承自UIView.在官方的文档中的描述是:
The backdrop for your app’s user interface and the object that dispatches events to your views.
用于提供应用程序背景以及分发事件给对应视图的对象。在之前的版本中,UIWindow可以直接使用,而不必设置UIWindow对象的rootViewController,在iOS 9.0版本之后,必须为UIWindow对象设置rootViewController才能正常使用,而不允许rootViewController属性为空.UIWindow对象主要通过rootViewController来管理需要展示的UIView的对象,也可以通过直接添加视图到UIWindow对象上的方法来展示.所以UIWindow类的主要作用有两个:
- 作为用户界面背景以及用户作为承载用户界面的容器;
- 分发用户交互事件,处理用户交互.
但是根据展示的需求,UIWindow可以显示在不同的互层级上,来保证需要的内容显示在最前面.UIWindow根据位置层级上的不同有三个级别:
- UIWindowLevelNormal:普通的展示应用界面的Window,是window的默认显示级别,对应的枚举值为0.000000;
- UIWindowLevelStatusBar:这个层级是要UIWindowLevelNormal高,在层次显示上会比UIWindowLevelNormal对应的window靠前一点,对应的枚举值为1000.000000;我个人觉得这个级别就是专门给UIStatusbar专用的,状态栏本身就有一个专门的UIWindow对象,所以状态栏可以显示在任何的控制器视图之前,而不会被遮挡;
- UIWindowLevelAlert:这个层级是最高的,会显示在最前端,也是留给开发者自定义展示内容使用的,对应的枚举值为2000.000000. 在iOS系统UI大量使用了这种类型的Window来展示弹窗,键盘等.
虽然系统给出的枚举只有这三个值,而事实上可用的层级值远远超过这个数量,你可以使用任意浮点类型来表示UIWindowLevel,比如100001.0.
UIWindow对象和视图控制器相互配合使用,可以处理各种用户交互事件以及执行对应用程序来说至关重要的操作.为了避免UIWindow的滥用,官方建议我们只能在以下场景中才可以使用UIWindow,
- 创建用来展示应用内容的主窗口(key window)
- 在必要的时候创建新的UIWindow来展示额外的内容.
对于第一个场景来讲,如果设置了main storyboard,Xcode会帮助你创建一个key window,并默认将main storyboard中的is initial viewController对象设置为key window的rootViewController;如果你未设置main storyboard那就需要手动创建key window,并为其设置对应的rootViewController来展示应用的界面.而对于第二个场景的应用就相对比较广泛,也在系统实图中被大量应用.例如UIAlertController,SKStoreReviewController,系统键盘等都是用了UIWindow作为控件显示的载体.
2. UIWindow与UIView
UIWindow是UIView的子类,所以UIWindow不仅继承了UIView的所有功能,还有一些UIView不具备的特性:
- UIWidow的展示不需要添加到任何父类视图上,直接创建就可以显示在对应的位置上,也正是由于系统不会自动持有创建的UIWindow对象,所以如果需要使用UIWindow来展示UI一定要确保在展示期间UIWindow对象的生命周期不被释放(同样的问题在计时器的相关使用中也会遇到);
- UIWindow的hidden默认属性是true,所以创建对象之后如果需要展示设置改属性为false;
- UIWindow对象必须要给rootViewController赋值之后才正常使用(在iOS 9.0之前可以直接使用UIWindow对象通过添加子视图来展示UI);
- UIWindow有keyWindow和非keyWindow的区分,只有keyWindow可以处理用户交互,分发事件.
正是由于UIWindow有这些特点,所以以下使用方法是错误的:
- 直接创建局部变量
- (void)tap:(UIButton *)sender { UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; window.windowLevel = UIWindowLevelAlert; window.rootViewController = [[MyViewController alloc] init]; [window makeKeyAndVisible]; //这个方法可以将当前window变成keyWindow,同时将hidden = false, 等价于: [window makeKeyWindow]; [window setHidden:false]; }
运行之后发现啥啥都没有显示,因为创建的window是局部变量,出了这个方法这个window变量就被释放了,也就不会有任何显示.
- 没有修改的默认hidden属性
- (void)tap:(UIButton *)sender { static UIWindow *window window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; window.windowLevel = UIWindowLevelAlert; window.rootViewController = [[MyViewController alloc] init]; [window makeKeyWindow]; }
由于UIWindow属性hidden的默认值为true,所以不会有任何显示.
- 如果只从可以显示的角度来讲,以下用法都可以算是正确的:
- (void)tap:(UIButton *)sender { //使用局部静态变量 static UIWindow *window; window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; window.windowLevel = UIWindowLevelAlert; window.rootViewController = [[MyViewController alloc] init]; [window makeKeyWindowAndVisible]; }
//使用全局静态变量 static UIWindow *window; - (void)tap:(UIButton *)sender { window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; window.windowLevel = UIWindowLevelAlert; window.rootViewController = [[MyViewController alloc] init]; window.hidden = false; }
- (void)tap:(UIButton *)sender { //设置为属性,将生命周期交给持有者管理 self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; window.windowLevel = UIWindowLevelAlert; window.rootViewController = [[MyViewController alloc] init]; [window makeKeyWindowAndVisible]; }
3. 使用makeKeyWindowAndVisible与直接设置hidden=false有什么不同?
通过以上分析,我们发现其实通过设置window.hidden = false与直接调用[window makeKeyWindowAndVisible]都可以实现展示UIWindow对象的功能,那么他们有什么区别呢?
makeKeyWindowAndVisible的功能等价于:
- hidden = false;
- makeKeyWindow.
所以主要的区别就在于makeKeyWindow这个方法.在一个应用中,我们有且只有只有一个keyWindow,keyWindow主要用于处理用户交互分发事件,如果一个UIWindow对象成为了keyWindow,它就会成为处理用户交互的主要主要窗口,而原来的keyWindow(如果存在的话)就会暂停接收用户交互.
那么让新创建的UIWindow成为keyWindow有什么好处呢?
好处就是我们可以简化一些处理,比如我们在当前界面唤起了键盘,此时突然如果需要显示自定义UIWindow的视图,如果你只是使用了hidden=false,就会发现键盘依然直愣愣杵在屏幕上,影响美观不说,还有可能会遮挡我们需要展示的内容.但是如果你使用了makeKeyWindowAndVisible,那么你就会发现在自定义UIWindow成为keyWindow的同时,键盘消失了.有人可能会说,我们使用手动处理键盘来可以达到同样的效果,为什么不建议使用呢?
- 我们之所以使用UIWindow来展示UI,很大一部分原因就是它可以独立于父视图而存在,不需要知道当前在显示的是那个视图(UIView或者UIWindow).如果我们来手动处理键盘事件,那么就需要键盘源控件所在的视图或者window,自定义UIWindow就失去了这个优势;
- 在应用代理中的创建的UIWindow对象是默认的keyWindow,当自定义window对象放弃成为keyWindow(调用resignKeyWindow)或者生命周期结束时,应用代理中创建UIWindow对象会自动成为keyWindow继续处理用户交互分发事件,这就使得我们只需要关注自定义UIWindow的行为即可,不需要关心自定义UIWindow对象消失之后谁来成为下一个keyWindow的问题,这带来极大的便利.
4. 什么情况下建议使用自定义的UIWindow
自定义UIWindow会在展示自定义UI上带来很大的便利,但并不主张在应用过多地使用,
- 过多使用自定义UIWindow会造成管理混乱.因为UIWindow对象不需要依赖父视图,所以需要对多个UIWidow对象生成唯一的标志符来进行区分和管理,会在管理上带来混乱,不便于操作;
- UIWindow可以自定义显示的层级,有可能会造成显示异常甚至完全被覆盖.
所以使用自定义UIWindow要慎之又慎,确保在同一时间只有一个自定义的UIWindow,且在使用结束之后及时销毁,不会有内存泄漏.在以下情况下,我们可以尝试使用自定义UIWIndow来展示内容:
- 用于展示临时性内容,且展示之后可以立即销毁内存;
- 展示内容时,原来的应用显示内容不受影响,对象空间不释放;
- 展示内容时,阻断原来window上的用户交互事件;
- 展示的内容在当前应用的window层级之上;
- 展示的界面不需要与原来window上界面有频繁的切换.
比较经典的应用场景有哪些?
- 在一些涉及到个人隐私信息的应用中,例如银行理财类应用,在应用挂起到后台时,当前界面会被一个遮罩层遮挡,防止用户信息泄漏;
- 手势解锁等身份验证功能;
- 需要展示在整个应用界面展示引起用户注意的信息.
4. 使用自定义UIWindow应该注意的事项
虽然自定义UIWindow对象来展示视图在有些场景下比较方便,但是并不是你想要的场景都可以使用自定义UIWindow对象来玩完成:
- 在使用自定义UIWindow对象时,需要注意windowLevel属性,一般情况下为了更好地显示信息,只需要比UIWindowLevelNormal稍高或者平级即可.
- 如果需要使用自定义UIWindow对象需要展示的界面功能非常复杂,且依赖第三方类库进行显示,就需要查看第三方类库的显示是否依赖应用中的window对象,避免显示异常.例如RKDropdownAlert的显示就依赖了应用的UIWindow来显示
... for (UIWindow *window in frontToBackWindows) if (window.windowLevel == UIWindowLevelNormal && !window.hidden) { [window addSubview:self]; break; } } ... ...
所以如果此时你使用了自定义的UIWindow对象来展示视图,且自定义UIWindow的windowLevel > UIWindowLevelNormal时就会发现RKDropdownAlert不能正常显示。