关于布局及屏幕适配,我们通常的做法是写多个宏定义用来定义状态栏高度、导航栏高度,每次布局时,通过宏定义来取得需要预留的高度,然后来确定控件实际的坐标。
举个:
//是否是刘海形屏幕
#define XAIsFringeScreen [UIScreen mainScreen].bounds.size.width >= 375.0f && [UIScreen mainScreen].bounds.size.height >= 812.0f
//状态栏高度
#define XAStatusBarHeight ([[UIApplication sharedApplication] statusBarFrame].size.height)
//导航栏高度(包含状态栏)
#define XANavigationBarHeight (44+XAStatusBarHeight)
//标签栏高度
#define XATabBarHeight ((XAIsFringeScreen) ? 83.0 : 49.0)
这么做的好处的什么或者说我们为什么这么做呢?
因为iPhone分为两种不同形状的屏幕,普通长方形屏幕以及刘海屏,刘海屏上方的刘海及下方的小黑条导致布局的时候,我们不能像普通的长方形屏幕一样去处理它们,必须避开一部分无法显示或者点击的区域,因此我们定义了多个宏定义来区分是否是刘海屏,及其两种屏幕下各自的导航栏、状态栏高度,方便快捷,便于修改扩展。
那么这么做的弊端又是什么呢?
- 无法一劳永逸的解决所有机型的适配问题,如果苹果新出了小尺寸刘海屏或者其他异形屏,我们都需要改动这些宏定义,甚至于改动具体文件中布局代码来进行重新适配。
- 这些宏定义只适用于竖屏布局,不适用于横屏,横屏时左右边距需要额外处理,导航栏、状态栏大小也与竖屏时不尽相同,横竖屏切换重新布局时,这些宏定义显然无法处理这种复杂的情况,还需要引入更多的宏定义已及加参。
- 无法适应所有情况,在低版本的iPhone机型中,后台定位、语音通话会导致系统状态栏高度改变,导航栏也随之下移一些,遮挡显示的视图。
既然已经发现了问题,那接下来就是如何解决这些问题了
iOS11的SafeArea安全区域属性
iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性,用来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length)。 这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。
一个独立的ViewController,不包含于任何其他的ViewController。如果状态栏可见,topLayoutGuide表示状态栏的底部,否则表示这个ViewController的上边缘。包含于其他ViewController的ViewController不对这个属性起决定作用,而是由容器ViewController决定这个属性的含义:
如果导航栏(Navigation Bar)可见,topLayoutGuide表示导航栏的底部。
如果状态栏可见,topLayoutGuide表示状态栏的底部。
如果都不可见,表示ViewController的上边缘。
从iOS 11 开始弃用了这两个属性, 并且引入了 SafeArea来代替它。SafeArea存在于UIViewController及UIView中,这里我们主要讨论UIViewController的SafeArea,UIView中的SafeArea与其大同小异。
SafeArea表示的安全区域已在上图中标出,SafeArea主要有两个属性
- safeAreaInsets 表示相对于屏幕四个边的间距
- safeAreaLayoutGuide 这是一个相对抽象的概念,我们可以把safeAreaLayoutGuide看成是一个“view”,它由safeAreaInsets设置的值计算而来,这个“view”系统自动帮我们调整它的bounds,让它不会被各种东西(status bar, navigation bar等)挡住,包括刘海区域和底部的一道杠区域,我们可以认为在这个“view”上一定能完整显示所有内容。
理解了这些概念我们就可以尝试利用这些属性,抽取一些属性及方法来让我们在ViewController中布局以及自定义视图中布局更加方便。
我们尝试抽取一个UIEdgeInsets类型的属性,用来统一表示ViewController中,子控件布局时应该距离各边界的距离;
在基类ViewController的.h中定义一个属性
/// 布局时的默认边距
@property (nonatomic , assign)UIEdgeInsets edgeInsets;
毫无疑问,设置这个属性的代码应该这么写
if (@available(iOS 11.0, *)) {
self.edgeInsets = self.view.safeAreaInsets;
}else{
self.edgeInsets = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
}
那么问题来了,我们知道如何设置这个属性,但是我们应该在哪个方法中设置它呢,是不是viewSafeAreaInsetsDidChange或者随便一个viewController生命周期方法来设置它都可以呢?
简单看一下各个方法执行时,safeAreaInsets的值
2020-04-27 23:50:31.091192+0800 XALayoutProject[2281:123399] -[ViewController viewDidLoad]--top:0.0,left:0.0,bottom:0.0,right:0.0
2020-04-27 23:50:31.108339+0800 XALayoutProject[2281:123399] -[ViewController viewWillAppear:]--top:0.0,left:0.0,bottom:0.0,right:0.0
2020-04-27 23:50:31.109407+0800 XALayoutProject[2281:123399] -[ViewController viewSafeAreaInsetsDidChange]--top:44.0,left:0.0,bottom:34.0,right:0.0
2020-04-27 23:50:31.115466+0800 XALayoutProject[2281:123399] -[ViewController viewWillLayoutSubviews]--top:44.0,left:0.0,bottom:34.0,right:0.0
2020-04-27 23:50:31.115664+0800 XALayoutProject[2281:123399] -[ViewController viewDidLayoutSubviews]--top:44.0,left:0.0,bottom:34.0,right:0.0
2020-04-27 23:50:31.162406+0800 XALayoutProject[2281:123399] -[ViewController viewDidAppear:]--top:44.0,left:0.0,bottom:34.0,right:0.0
可以看到,viewSafeAreaInsetsDidChange调用时机很早,在viewWillAppear后,并且VC的safeAreaInsets直到viewSafeAreaInsetsDidChange调用前,都是UIEdgeInsetsZero,之后才是正确的UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0),
并且viewSafeAreaInsetsDidChange后面会调用两次viewDidLayoutSubviews,所以我们应该设置这个属性的代码写在viewDidLayoutSubviews或者viewSafeAreaInsetsDidChange里,把改变高度或布局的代码都写在viewDidLayoutSubviews里,这样edgeInsets的值才是正确的。