浅谈iOS的多Window处理

简介: ### 概述 想必做iOS的人都知道,我们的App是通过UIWindow这个载体呈现出来的。默认情况下,iOS App对于开发者来说只有一个`UIWindow`,也就是AppDelegate在`applicationDidFinishLaunching`里面创建出来的。 但是即使我们什么都不做,在我们的APP里面也会有其他的`UIWindow`: 1. 键盘对应的UITextEffe

概述

想必做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
目录
相关文章
|
Swift iOS开发
iOS 13 之后自定义 Window 不显示解决 (SceneDelegate)
iOS 13 之后自定义 Window 不显示解决 (SceneDelegate)
378 0
|
iOS开发
iOS 获取当前window(最上层window)
iOS 获取当前window(最上层window)
788 0
|
安全 数据安全/隐私保护 iOS开发
uniapp开发,window下创建ios打包证书的详情流程
uniapp开发,window下创建ios打包证书的详情流程
|
iOS开发
【iOS开发】whose view is not in the window hierarchy
在做界面跳转的时候,我们经常会用到这两个函数 func dismissViewControllerAnimated(flag:Bool, completion: (() ->Void)?) func presentViewController(vie...
1217 0
|
1月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
9天前
|
iOS开发 开发者 MacOS
深入探索iOS开发中的SwiftUI框架
【10月更文挑战第21天】 本文将带领读者深入了解Apple最新推出的SwiftUI框架,这一革命性的用户界面构建工具为iOS开发者提供了一种声明式、高效且直观的方式来创建复杂的用户界面。通过分析SwiftUI的核心概念、主要特性以及在实际项目中的应用示例,我们将展示如何利用SwiftUI简化UI代码,提高开发效率,并保持应用程序的高性能和响应性。无论你是iOS开发的新手还是有经验的开发者,本文都将为你提供宝贵的见解和实用的指导。
91 66
|
19天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
24天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
25天前
|
存储 前端开发 Swift
探索iOS开发:从新手到专家的旅程
本文将带您领略iOS开发的奇妙之旅,从基础概念的理解到高级技巧的掌握,逐步深入iOS的世界。文章不仅分享技术知识,还鼓励读者在编程之路上保持好奇心和创新精神,实现个人成长与技术突破。
|
28天前
|
安全 IDE Swift
探索iOS开发之旅:从初学者到专家
在这篇文章中,我们将一起踏上iOS开发的旅程,从基础概念的理解到深入掌握核心技术。无论你是编程新手还是希望提升技能的开发者,这里都有你需要的指南和启示。我们将通过实际案例和代码示例,展示如何构建一个功能齐全的iOS应用。准备好了吗?让我们一起开始吧!