浅谈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)
346 0
|
iOS开发
iOS 获取当前window(最上层window)
iOS 获取当前window(最上层window)
748 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...
1207 0
|
13天前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
54 1
|
20天前
|
设计模式 安全 Swift
探索iOS开发:打造你的第一个天气应用
【9月更文挑战第36天】在这篇文章中,我们将一起踏上iOS开发的旅程,从零开始构建一个简单的天气应用。文章将通过通俗易懂的语言,引导你理解iOS开发的基本概念,掌握Swift语言的核心语法,并逐步实现一个具有实际功能的天气应用。我们将遵循“学中做,做中学”的原则,让理论知识和实践操作紧密结合,确保学习过程既高效又有趣。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你打开一扇通往iOS开发世界的大门。
|
21天前
|
搜索推荐 IDE API
打造个性化天气应用:iOS开发之旅
【9月更文挑战第35天】在这篇文章中,我们将一起踏上iOS开发的旅程,通过创建一个个性化的天气应用来探索Swift编程语言的魅力和iOS平台的强大功能。无论你是编程新手还是希望扩展你的技能集,这个项目都将为你提供实战经验,帮助你理解从构思到实现一个应用的全过程。让我们开始吧,构建你自己的天气应用,探索更多可能!
43 1
|
29天前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
1月前
|
开发框架 数据可视化 Java
iOS开发-SwiftUI简介
iOS开发-SwiftUI简介
|
14天前
|
移动开发 前端开发 Swift
iOS 最好的应用程序开发编程语言竟然是这7种
iOS 最好的应用程序开发编程语言竟然是这7种
49 8