002 iOS 11 Safe Area(安全距离) 是个什么屎
-
检索词条:safeAreaInsets iOS 11 安全距离 适配iPhoneX viewSafeAreaInsetsDidChange iOS 11安全距离什么时候被改变
-
环境 iOS 11 Xcode9.0 以上(这里也会介绍如果你和你的同事两个人的Xcode一个是9.0以上 一个是9.0以下情况的解决办法)
-
上一篇中我们讲了safeAreaInsets的改变时机 及在不同机型上的变化 这里我们通过不同的示例来展示不同情况下的安全距离问题
看这个例子: ** 拖两个自定义的 View, 这个 View 上有一个 显示很多字的Label。然后设置这两个 View 的约束分别是:
1.不考虑安全距离
// 不考虑安全距离
lable_1.frame = CGRectMake(0, 0, SCREEN_WIDTH, lable_1.height);
lable_2.frame = CGRectMake(0, SCREEN_HEIGHT - lable_2.height,SCREEN_WIDTH, lable_2.height);
Snip20171104_9.png
可以看出来, 子视图被顶部的刘海以及底部的 home 指示区挡住了。我们可以使用 frame 布局或者 auto layout 来优化这个地方:
解决方案
- 方案一 在
viewSafeAreaInsetsDidChange
或之后的方法中布局这连个lable
- (void)viewSafeAreaInsetsDidChange{
[super viewSafeAreaInsetsDidChange];
// 考虑安全距离
UIEdgeInsets insets = self.view.safeAreaInsets;
_lable_1.frame = CGRectMake(insets.left, self.view.safeAreaInsets.top, SCREEN_WIDTH - insets.left - insets.right, _lable_1.height);
_lable_2.frame = CGRectMake(insets.left, SCREEN_HEIGHT - _lable_2.height - insets.bottom,SCREEN_WIDTH - insets.left - insets.right, _lable_2.height);
NSLog(@"viewSafeAreaInsetsDidChange__self.view.safeAreaInsets = %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
}
Snip20171104_11.png
- 方案二 直接在自定义的 View 中修改 Label 的布局:
safeAreaInsetsDidChange
- (void)safeAreaInsetsDidChange{
NSLog(@"layoutSubviews__self.safeAreaLayoutGuide.layoutFrame = %@",NSStringFromCGRect(self.safeAreaLayoutGuide.layoutFrame));
CGFloat safeTop = self.safeAreaLayoutGuide.layoutFrame.origin.y;
CGFloat safeBottom = SCREEN_HEIGHT - self.safeAreaLayoutGuide.layoutFrame.size.height - safeTop;
_lable_1.frame = CGRectMake(0, safeTop, SCREEN_WIDTH, _lable_1.height);
}
Snip20171104_13.png
这样, 不仅仅是在 ViewController 中能够使用 safe area 了。
UIViewController 中的 safe area
- 在 iOS 11 中 UIViewController 有一个新的属性
@property(nonatomic) UIEdgeInsets additionalSafeAreaInsets API_AVAILABLE(ios(11.0), tvos(11.0));
当 view controller 的子视图覆盖了嵌入的子 view controller 的视图的时候。比如说, 当 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 状态的时候, 就有 additionalSafeAreaInsets
Snip20171104_15.png- (void)viewSafeAreaInsetsDidChange NS_REQUIRES_SUPER API_AVAILABLE(ios(11.0), tvos(11.0));
- (void)safeAreaInsetsDidChange API_AVAILABLE(ios(11.0),tvos(11.0));
这两个方法分别是 UIView 和 UIViewController 的 safe area insets 发生改变时调用的方法,如果需要做一些处理,可以重写这个方法。有点类似于 KVO 的意思。
模拟 iPhone X 的 safe area
//竖屏
additionalSafeAreaInsets.top = 24.0
additionalSafeAreaInsets.bottom = 34.0
//竖屏, status bar 隐藏
additionalSafeAreaInsets.top = 44.0
additionalSafeAreaInsets.bottom = 34.0
//横屏
additionalSafeAreaInsets.left = 44.0
additionalSafeAreaInsets.bottom = 21.0
additionalSafeAreaInsets.right = 44.0
UIScrollView 中的 safe area
iOS 7 中引入 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性在 iOS11 中被废弃掉了。取而代之的是 UIScrollView 的 contentInsetAdjustmentBehavior
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));
这是一个枚举
UIScrollViewContentInsetAdjustmentBehavior
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic, // 默认值
UIScrollViewContentInsetAdjustmentScrollableAxes, //
UIScrollViewContentInsetAdjustmentNever, //
UIScrollViewContentInsetAdjustmentAlways, //
} API_AVAILABLE(ios(11.0),tvos(11.0));
-
never
不做调整。 -
scrollableAxes content insets
只会针对 scrollview 滚动方向做调整。 -
always content insets
会针对两个方向都做调整。 -
automatic
这是默认值。当下面的条件满足时, 它跟 always 是一个意思
能够水平滚动,不能垂直滚动
scroll view 是 当前 view controller 的第一个视图
这个controller 是被navigation controller 或者 tab bar controller 管理的
automaticallyAdjustsScrollViewInsets 为 true
在其他情况下automoatc 跟 scrollableAxes 一样
Adjusted Content Insets
iOS 11 中 UIScrollView 新加了一个属性:adjustedContentInset
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));
- adjustedContentInset 和 contentInset 之间有什么区别呢?
在同时有 navigation 和 tab bar 的 view controller 中添加一个 scrollview 然后分别打印两个值:
//iOS 10
//contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
//iOS 11
//contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
//adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
然后再设置:
// 给 scroll view 的四个方向都加 10 的间距
scrollView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
打印:
//iOS 10
//contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
//iOS 11
//contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
//adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
由此可见,在 iOS 11 中 scroll view 实际的 content inset 可以通过 adjustedContentInset 获取。这就是说如果你要适配 iOS 10 的话。这一部分的逻辑是不一样的。
系统还提供了两个方法来监听这个属性的改变
UIScrollView
- (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0),tvos(11.0)) NS_REQUIRES_SUPER;
代理:
@protocol UIScrollViewDelegate<NSObject>
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0), tvos(11.0));
UITableView 中的 safe area
Snip20171106_7.png自定义的 header 上面有一个 lable,自定义的 cell 上面也有一个 label。将屏幕横屏之后会发现,cell 以及 header 的布局均自动留出了 safe area 以外的距离。cell 还是那么大,只是 cell 的 contnt view 留出了相应的距离。这其实是 UITableView 中新引入的属性管理的:
insetsContentViewsToSafeArea
insetsContentViewsToSafeArea
的默认值是 true, 将其设置成 no 之后:Snip20171106_8.png
UICollectionView 中的 safe area
我们在做一个相同的 collection view 来看一下 collection view 中是什么情况:
Snip20171106_9.png这是一个使用了 UICollectionViewFlowLayout 的 collection view。 滑动方向是竖向的。cell 透明, cell 的 content view 是白色的。这些都跟上面 table view 一样。header(UICollectionReusableView) 没有 content view 的概念, 所以给其自身设置了红色的背景。
从截图上可以看出来, collection view 并没有默认给 header cell footer 添加safe area 的间距。能够将布局调整到合适的情况的方法只有将 header/ footer / cell 的子视图跟其 safe area 关联起来。
Snip20171106_10.png
现在我们再试试把布局调整成更像 collection view 那样:
1508844448536015.jpg
截图上可以看出来横屏下, 左右两边的 cell 都被刘海挡住了。这种情况下, 我们可以通过修改 section insets 来适配 safe area 来解决这个问题。但是再 iOS 11 中, UICollectionViewFlowLayout 提供了一个新的属性
sectionInsetReference
来帮你做这件事情。
@property (nonatomic) UICollectionViewFlowLayoutSectionInsetReference sectionInsetReference API_AVAILABLE(ios(11.0), tvos(11.0)) API_UNAVAILABLE(watchos);
typedef NS_ENUM(NSInteger, UICollectionViewFlowLayoutSectionInsetReference) {
UICollectionViewFlowLayoutSectionInsetFromContentInset,
UICollectionViewFlowLayoutSectionInsetFromSafeArea,
UICollectionViewFlowLayoutSectionInsetFromLayoutMargins
} API_AVAILABLE(ios(11.0), tvos(11.0)) API_UNAVAILABLE(watchos);
可以看出来,系统默认是使用 .fromContentInset 我们再分别修改, 看具体会是什么样子的。
Snip20171106_11.png这种情况下 section content insets 等于原来的大小加上 safe area insets 的大小。
跟使用 .fromLayoutMargins 相似使用这个属性 colection view 的 layout margins 会被添加到 section content insets 上面。
总结
1.在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只会给之后的工作代码更多的麻烦。
2.如果只需要适配到 iOS9 之前的 storyboard 都只需要做一件事情。
3.Xcode9 用 IB 可以看得出来, safe area 到处都是了。理解起来很简单。就是系统对每个 View 都添加了 safe area, 这个区域的大小,是否跟 view 的大小相同是系统来决定的。在这个 View 上的布局只需要相对于 safe area 就可以了。每个 View 的 safe area 都可以通过 iOS 11 新增的 API safeAreaInsets 或者 safeAreaLayoutGuide 获取。
4.对与 UIViewController 来说新增了 additionalSafeAreaInsets 这个属性, 用来管理有 tabbar 或者 navigation bar 的情况下额外的情况。
5.对于 UIScrollView, UITableView, UICollectionView 这三个控件来说,系统以及做了大多数的事情。
scrollView 只需要设置 contentInsetAdjustmentBehavior 就可以很容易的适配带 iPhoneX
tableView 只需要在 cell header footer 等设置约束的时候相对于 safe area 来做
对 collection view 来说修改 sectionInsetReference 为 .safeArea 就可以做大多数的事情了。
6.总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(当然是在 iPhoneX上才有值) 以后在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局。