浅谈iOS的多Window处理

概述

想必做iOS的人都知道,我们的App是通过UIWindow这个载体呈现出来的。默认情况下,iOS App对于开发者来说只有一个UIWindow,也就是AppDelegate在applicationDidFinishLaunching里面创建出来的。

但是即使我们什么都不做,在我们的APP里面也会有其他的UIWindow:

  1. 键盘对应的UITextEffectWindow
  2. 状态栏对应的UIStatusBarWindow(准确来说这个Window并不隶属于我们的App)

只不过上述两种UIWindow我们一般不太容易去操作罢了,因此很多问题都无形被掩盖住了。最近正好需要做双十一晚会一个和横屏界面相关的需求,在整个过程中,发现了不少问题,所以接下来我们就说说如果在多个UIWindow状态下存在的一些问题吧。

那么在什么情况下会导致我们想要创建多UIWindow的状态呢?我总结了一下,包括但不限于:

  1. 全局性的自定义HUD,Alert效果(SCAlert)等等。
  2. 需要展示的界面需要盖住UIStatusBar。

其中,第一种需求其实不一定需要创建一个新的UIWindow实例,我们也可以将这些自定义的全局性界面添加到AppDelegate的window上。但是这样就会产生一个问题,由于在iOS8之前,UIWindow的bounds是不会随着旋转而改变的,拿到的永远是处于Portrait模式下的坐标系坐标。因此,对于直接添加在UIWindow上的视图,我们需要自己根据 UIApplicationDidChangeStatusBarOrientationNotification来进行转换处理。

苹果这篇Q&A讲述了比较具体的原因:UIWindow并不会处理rotation事件,而是UIWindow的rootViewController去处理。

而对于第二种问题,添加一个盖在UIStatusBar上的界面,就必须依赖我们自己创建一个新的UIWindow,究其原因在于UIStatusBar本身并不属于我们App内可控的一个控件,而是一个系统级创建出来的产物。
因此,我们必须创建一个WindowLevel大于UIWindowStatusBar的新Window盖在上面才行。

有人会问:咦,奇怪了,为什么你在自己App内添加一个WindowLevel大于statusbar的就可以了呢?你只是在你自身应用内添加了一个UIView(UIWindow的子类),竟然能影响系统级的控件?

不知道大家有没有了解过CALayer这层有个属性叫zPosition。通过操纵这个属性,我们可以调整视图渲染的前后关系。即使有的UIView在构建层级树的时候被后加的UIView所遮盖,但是在构建渲染树的时候,zIndex越高的视图就会越处于视觉前方进行渲染。 而渲染树构建完成之后,并不是在我们的App内部进行渲染,而是通过IPC通信,统一交由一个第三方进程Render Server进行渲染。而在我们这里处理盖住StatusBar的多Window的情形也是基于这个原理进行。

横屏及旋转

现在绝大多数的iPhone应用都是竖屏应用,即只支持Portrait模式。但是随着视频、直播的风口到来,在新闻、购物等等APP内都会插入视频播放这一特性,而视频播放需要的全屏播放特性势必要用到横屏,也就意味着会牵扯到旋转。

横屏旋转分为两种,一种是强制性的,一种是随着设备进行旋转的。什么意思呢?
大家还记得手机上有旋转锁这一个开关吧,你将旋转锁开启的时候,手机就保持在锁定对应的模式下,无法自动根据你旋转设备而旋转。在这种模式下,如果你需要更改APP界面对应的UIInterfaceOrientation,就必须要么在对应的viewcontroller里面提供实现如下的方法:

- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskLandscapeRight; // 表示支持水平右方向
}

- (BOOL)shouldAutorotate
{
    return YES;
}

这样,当你展现到这个页面的时候,就会触发系统检查一下当前页面应该所处的Orientation,从而达到正确的显示效果。

但需要注意的是,如果你的界面是处于一个UINavigationController或者UITabbarController内的话,你就需要从父容器开始,写对应的supportedInterfaceOrientations实现,否则就无法得到正确的效果。

PS: 其实这个道理和hideBottomBarWhenPushed是一个道理。很多人用了这个属性,发现隐藏Tabbar的时机经常错乱了,这个就在于没有仔细阅读文档,需要在整个导航栈里面的topmostViewController提供正确的属性设置才行。

The value of this property on the topmost view controller determines whether the toolbar is visible. If the value of this property is true, the toolbar is hidden. If the value of this property is false, the bar is visible

或者你可以将你需要横屏的ViewController通过present的形势展现出来(有人觉得会狠突兀,那你自己实现专场动画过渡就可以了)。不过呢,这种实现方式会有一个超级大坑,待会我们细细说。

上面这种就是强制性的。

而自动旋转的就是打开旋转锁,让界面随着设备的旋转而进行旋转,这种旋转是物理特性的,非强制性的。

Q: 那么这两种旋转的区别在哪?
A: UIInterfaceOrientation(UIStatusBar的所处方向)和UIDeviceOrientation是否一致。

Q: 那么有什么问题呢?
A: 在iOS8之后,UIScreen的bounds是随着物理设备的旋转而更改的。如果你需要获取iOS8之前的bounds效果,需要使用nativeBounds。但是要记得,nativeBounds是像素级别的,你需要换算到对应的point单位来,所以关系是:

bounds( < iOS8.0) = nativeBounds / nativeScale;

大家可以参考苹果的文档来更确切的掌握一下。

上面的内容我们曾经提及在采用多UIWindow时候的几个大坑,如果你现在有自定义的界面,想要添加到除了delegate window之外的window,可能会遇到如下几个问题。

直接将自定义的视图作为Subview添加到UIWindow上

从理论上来说UIWindow继承于UIView,这种直接用法在认知上没有任何的问题。但是如果涉及的应用牵扯到横屏模式而且又要支持iOS7的话(我相信现在没有哪个产品还需要支持iOS6)吧,那么针对iOS7需要单独处理横屏的坐标系转换。我们摘录一段著名的开源库MBProgressHUD的代码作为示例:

#if __IPHONE_OS_VERSION_MIN_REQUIRED < 80000
    // Only needed pre iOS 8 when added to a window
    BOOL iOS8OrLater = kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_8_0;
    if (iOS8OrLater || ![self.superview isKindOfClass:[UIWindow class]]) return;

    // Make extension friendly. Will not get called on extensions (iOS 8+) due to the above check.
    // This just ensures we don't get a warning about extension-unsafe API.
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if (!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) return;

    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    UIInterfaceOrientation orientation = application.statusBarOrientation;
    CGFloat radians = 0;

    if (UIInterfaceOrientationIsLandscape(orientation)) {
        radians = orientation == UIInterfaceOrientationLandscapeLeft ? -(CGFloat)M_PI_2 : (CGFloat)M_PI_2;
        // Window coordinates differ!
        self.bounds = CGRectMake(0, 0, self.bounds.size.height, self.bounds.size.width);
    } else {
        radians = orientation == UIInterfaceOrientationPortraitUpsideDown ? (CGFloat)M_PI : 0.f;
    }

        self.transform = CGAffineTransformMakeRotation(radians);
#endif

通过rootViewController的view添加子视图

这种方式就是通过将window.rootViewController = vc,然后我们所有的子视图都添加到vc.view

这种使用的好处是我们无需去考虑版本兼容的问题,通过vc.view拿到的坐标系对于我们来说都是和UIInterfaceOrientation正确转换过的。

在iOS7之前,坐标系的转换是系统通过设置vc.transform更改;而在iOS8之后,vc和window的旋转会根据UIDeviceOrientation和viewcontroller自身supportedInterfaceOrientations进行交集的操作。

总之,需要支持横屏的自定义界面,全部放在viewcontroller.view上来做,是准没错的。

而且,在iOS9以后,苹果推荐每个UIWindow都必须有一个rootViewController。否则在启动过程使用了不包含rootViewController的UIWindow中会导致必现的crash。

presentViewController的大坑

我们前面提过,如果想要让viewcontroller单独横屏有两种方式。

  1. 如果你的界面是处于一个UINavigationController或者UITabbarController内的话,你就需要从父容器开始,写对应的supportedInterfaceOrientations实现,否则就无法得到正确的效果。
  2. 或者你可以将你需要横屏的ViewController通过present的形势展现出来

第二种方案在实现过程中,会产生一个非常隐晦的大坑,容我慢慢道来。
首先我们需要了解下整体响应旋转变化的事件流程,简单来说如下:

UIScreen -> UIWindow -> UIViewController -> ChildViewControllers -> View -> Subviews

其中,UIWindow对应的处理方法是:supportedInterfaceOrientationsForWindow;而UIViewController对应的处理方法是supportedInterfaceOrientations

也就是说,当系统通过这个流程向我们请求界面的UIInterfaceOrientation的时候,我们必须确保我们能够提供正确的返回参数。

而这个流程在使用presentViewController弹出modalViewController会产生一些问题:即当你想从modalViewController 返回(dismiss)原先的界面的时候,你会发现虽然原先界面强制设置了portrait模式,但是如果设备锁关闭且设备仍然处于水平状态,那么此时的UIInterfaceOrientation,仍然是不准确的。

其原因在于:当你想要dismiss的时候,系统的确发起了一次新的请求流程。但是此时,modalViewController正处于dismissing的状态中,请求到的supportedInterfaceOrientations还是针对modalViewController的。所以,如果你的modalViewController是横屏模式,那么返回后的效果就是横屏模式,除非你人为的旋转一下设备,让其回到竖直方向。

Q: 那么这种问题有没有解决办法呢?
A: 你可以在supportedInterfaceOrientations里面判断下当前的viewcontroller是不是处于isBeingDismissed,如果是的话,取其presentingViewControllersupportedInterfaceOrientations作为返回值。

Q: 有些同学会问,我们怎么从来没遇到过这个问题?
A: 那是因为你们使用的UIWindow 99%的可能都是默认的delegate window,对于这个window,所有的旋转事件都自动帮你校准了,因此无需担忧。

参考资料

  1. UIWindow in iOS
  2. After rotation UIView coordinates are swapped but UIWindow's are not?
  3. 详解UICoordinateSpace和UIScreen在iOS 8上的坐标问题
  4. iOS 7+ Dismiss Modal View Controller and Force Portrait Orientation
  5. iOS Orientations: Landscape orientation for only one View Controller
时间: 2024-11-08 17:53:31

浅谈iOS的多Window处理的相关文章

浅谈iOS Crash(一)

本文讲的是浅谈iOS Crash(一),一.捕获iOS Crash 1.设置异常断点并运行 设置异常断点.png 说明:设置Xcode异常断点后运行程序,发生Crash时,断点会定位到出错的代码行,但仅适用于开发阶段.线上APP的Crash还需要通过收集Crash机制来捕获Crash并记录在日志中. 2.Mach异常 和 Unix信号 iOS Crash发生时,先产生Mach异常(最底层的内核级异常),然后Mach异常在host层被ux_exception转换为相应的Unix信号,并通过thre

浅谈iOS Crash(二)

浅谈iOS Crash(一) 一.僵尸对象(Zombie Objects) 1.概述 僵尸对象:已经被释放掉的对象.一般来说,访问已经释放的对象或向它发消息会引起错误.因为指针指向的内存块认为你无权访问或它无法执行该消息,这时候内核会抛出一个异常( EXC ),表明你不能访问该存储区域(BAD ACCESS).(EXC_BAD_ACCESS类型错误) 调试解决该类问题一般采用NSZombieEnabled(开启僵尸模式). 2.使用NSZombieEnabled Xcode提供的NSZombie

IOS中 浅谈iOS中MVVM的架构设计与团队协作

今天写这篇文章是想达到抛砖引玉的作用,想与大家交流一下思想,相互学习,博文中有不足之处还望大家批评指正.本篇文章的内容沿袭以往博客的风格,也是以干货为主,偶尔扯扯咸蛋(哈哈~不好好工作又开始发表博客啦~). 每日更新关注:http://weibo.com/hanjunqiang  新浪微博 由于本人项目经验有限,关于架构设计方面的东西理解有限,我个人对MVVM的理解主要是借鉴于之前的用过的MVC的Web框架~在学校的时候用过ThinkPHP框架,和SSH框架,都是MVC的架构模式,今天MVVM与

浅谈iOS和Android的产品交互设计:iPhone和Android的控件对比

一. 前言 开发者在产品上同时覆盖iOS平台和Android平台时,则会遇到同一个功能在不同平台中界面和交互如何展现的问题. 了解两个平台间的控件对应关系和异同点,对同时面向两个平台的产品和交互设计是有帮助的. 此部分就两个平台的控件进行对应,并辅以一定的描述,更详细的控件说明和适用的场合请直接参阅参考资料中的相关文档. 说明:Android中的系统控件会根据不同的ROM和操作系统版本有所变化,本文中Android控件以<Android Design>为主要参考. 二.目的 本文期望表达的含义

浅谈iOS中三种生成随机数方法_IOS

ios 有如下三种随机数方法: //第一种 srand((unsigned)time(0)); //不加这句每次产生的随机数不变 int i = rand() % 5; //第二种 srandom(time(0)); int i = random() % 5; //第三种 int i = arc4random() % 5 ; 注: ① rand()和random()实际并不是一个真正的伪随机数发生器,在使用之前需要先初始化随机种子,否则每次生成的随机数一样.       ② arc4random

浅谈iOS单元测试

什么是单元测试? 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证.对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等.总的来说,单元就是人为规定的最小的被测功能模块. iOS中单元测试有什么意义? 大型项目开发,功能比较繁琐,代码量比较大,调试某一块功能需要不断的Command+R运行调试,很显然这样的功能是非常非常的低效的,编写过程中以及App功能完成后

浅谈iOS模糊效果

前言 今年过年微信客户端临时出现了一个非常有意思的功能,好友发出带了一层蒙层的照片,发指定金额的红包即可看到清晰相片,今天我们就解开这个神秘的技术面纱. 实现技术汇总 官方: CoreImage(很早就出现的技术,缺点是转化效率低) UIBlurEffect(iOS8以后出现的新技术,效率比上面的要好,并且简单) VImage 苹果提供了一个庞大的库,进行更复杂的模糊的效果实现 非官方: GPUImage(第三方大神写的框架) 让我们浅析一下上面的几种技术 1.CoreImage imageVi

iOS开发之浅谈MVVM的架构设计与团队协作

今天写这篇博客是想达到抛砖引玉的作用,想与大家交流一下思想,相互学习,博文中有不足之处还望大家批评指正.本篇博客的内容沿袭以往博客的风格,也是以干货为主,偶尔扯扯咸蛋(哈哈~不好好工作又开始发表博客啦~). 由于本人项目经验有限,关于架构设计方面的东西理解有限,我个人对MVVM的理解主要是借鉴于之前的用过的MVC的Web框架~在学校的时候用过ThinkPHP框架,和SSH框架,都是MVC的架构模式,今天MVVM与传统的MVC可谓是极为相似,也可以说是兄弟关系,也就是一家人了. 说道架构设计和团队

浅谈window.onbeforeunload() 事件调用ajax_jquery

经常有这样的需求,就是在离开某个web页面时,用户不一定点注销,这样会导致会话不能及时销毁.为实现用户离开页面时,自动注销功能,需要在web页面的onbeforeunload事件处理函数中发送注销命令.这个地方大多用Ajax实现.有时还涉及到跨域访问的问题.这个地方就存在浏览器的兼容性问题. 浏览器在处理这个需求时的不兼容性有如下两点: 1.处理Ajax时的不兼容性,这里使用jQuery来解决. 2.在发送Ajax请求时的不兼容性 主要代码如下: function logout() { var