在 iOS 7 Apple 在 UIViewController 中引入了 topLayoutGuide
和 bottomLayoutGuide
属性来描述没有被覆盖(status bar, navigation bar, toolbar, tab bar, etc.)屏幕的区域。在 iOS 11 中,Apple 已经弃用了这些属性,并引入了 safe area。Apple 建议我们不要在 safe area 操作,在 iOS 11 中,当在 iOS App 中定位视图时,你必须使用新的 safe area API。
UIView
在 iOS 11 UIViewController topLayoutGuide
和 bottomLayoutGuide
属性已经被替换成了新的 UIView 中的 safe area:
@available(iOS 11.0, *)
open var safeAreaInsets: UIEdgeInsets { get }
@available(iOS 11.0, *)
open var safeAreaLayoutGuide: UILayoutGuide { get }
safeAreaInsets
属性意味着屏幕可以覆盖从四个方向,而不仅仅是顶部和底部。当被 iPhone X 呈现时,我们就明白了为什么我们需要左右 insets。

iPhone 8 vs iPhone X safe area (portrait orientation)

iPhone 8 vs iPhone X safe area (landscape orientation)
iPhone X 在 portrait orientation 有 top 和 bottom 的 safe area,在 landscape orientation 有 left right 和 bottom。
让我们来看一个例子。在 ViewController 的 View 的顶部和底部添加了两个带有文本标签和固定高度的 custom subviews,并附加 attached 到视图的边缘 edges。

Subviews are attached to the view’s edges
正如所看到的,subviews 内容与顶部的 notch 和底部的 home indicator 指示器重叠 overlapped。为了正确地定位 subviews,我们可以使用手动布局将它们附加到 safe area:
topSubview.frame.origin.x = view.safeAreaInsets.left
topSubview.frame.origin.y = view.safeAreaInsets.top
topSubview.frame.size.width = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right
topSubview.frame.size.height = 300
或者使用 Auto Layout:
bottomSubview.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
bottomSubview.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
bottomSubview.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
bottomSubview.heightAnchor.constraint(equalToConstant: 300).isActive = true

Subviews are attached to the superview safe area
上面看起来好很多。此外可以在 subview subclass 添加 subviews content 到 safe area。
// 方法一:
label.translatesAutoresizingMaskIntoConstraints = false
label.leftAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leftAnchor).isActive = true
label.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor).isActive = true
label.rightAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.rightAnchor).isActive = true
// 或者方法二:
override func layoutSubviews() {
super.layoutSubviews()
var labelFrame = bounds
labelFrame.origin.x += safeAreaInsets.left
labelFrame.origin.y += safeAreaInsets.top
labelFrame.size.width -= safeAreaInsets.left + safeAreaInsets.right
labelFrame.size.height -= safeAreaInsets.top + safeAreaInsets.bottom
label.frame = labelFrame
}

Subviews are attached to the view’s edges. Labels are attached to the superview safe area.
在 subviews 层次结构 hierarchy 的任何地方都可以将 view 添加到 safe area。
UIViewController
在 iOS 11 UIViewController 有了一个新属性:
@available(iOS 11.0, *)
open var additionalSafeAreaInsets: UIEdgeInsets
当 view controller subviews 覆盖嵌入的 child view controller views 时,将使用它。例如,Apple 在 UINavigationController 和 UITabBarController 中使用额外的 additional safe area insets,当这些条是半透明的。
additionalSafeAreaInsets
是对现有 safearea 的扩展附加。
当你改变 additional safe area insets 或者 safe area insets 被系统改变,UIView 和 UIViewController 中的方法将被调用:
// UIView
@available(iOS 11.0, *)
open func safeAreaInsetsDidChange()
//UIViewController
@available(iOS 11.0, *)
open func viewSafeAreaInsetsDidChange()
需要注意的是,当状态栏隐藏时,可能会出现一个奇怪的问题:所有的 safe area insets 计算都是正确的,但导航栏会移动到顶部的 notch 下方。这是一个 bug,目前没有很好的修复方法,只能使用一些变通方法。
Simulate iPhone X safe area
Additional safe area insets 也可以用来测试你的 app 是如何支持 iPhone X,如果你不能在模拟器上测试你的 app,而且没有 iPhone X,那就很有用了。
//portrait orientation, status bar is shown
additionalSafeAreaInsets.top = 24.0
additionalSafeAreaInsets.bottom = 34.0
//portrait orientation, status bar is hidden
additionalSafeAreaInsets.top = 44.0
additionalSafeAreaInsets.bottom = 34.0
//landscape orientation
additionalSafeAreaInsets.left = 44.0
additionalSafeAreaInsets.bottom = 21.0
additionalSafeAreaInsets.right = 44.0

UIScrollView
在 iOS 11 中,UIScrollView 的 insets 调整行为有了重大变化。让我们添加一个带有文本标签的 scroll view 到 view controller 并将其附加到视图的边缘。
你会发现 scroll view 的 insets 在顶部和底部会自动调整。在 iOS 7 及更高版本中,scroll view content insets 调整行为可以通过 UIViewController 的 automaticallyAdjustsScrollViewInsets
属性管理,但在 iOS 11 中,它被弃用并由新的 UIScrollView 的 contentInsetAdjustmentBehavior
属性替代:
@available(iOS 11.0, *)
public enum ContentInsetAdjustmentBehavior : Int {
case automatic
case scrollableAxes
case never
case always
}
@available(iOS 11.0, *)
open var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior
Content Insets Adjustment Behavior
never — scroll view content insets 永远不会被调整,很简单。
scrollableAxes — 只为可滚动的轴调整 content insets。例如,当 scroll view content size height 大于 frame size height 或
alwaysBounceVertical
属性启用时,垂直轴是可滚动的。同样,当 content size width 大于 frame size width 或alwaysBounceHorizontal
属性启用时,水平轴是可滚动的。
例如,在横屏方向,如果水平轴不可滚动,只有底部 content inset 会被调整,左右 content insets 不会被调整。
always — 对所有可滚动和不可滚动的轴调整 scroll view content insets。
automatic — 默认值,也是最有趣的值。当以下条件为真时,它与
always
相同:- scroll view 水平轴可滚动,垂直轴不可滚动
- scroll view 是 view controller 视图的第一个子视图
- view controller 是导航控制器或标签栏控制器的子视图
automaticallyAdjustsScrollViewInsets
启用
在所有其他情况下,automatic
与 scrollableAxes
相同。
automatic
行为是默认的,因为它具有向后兼容性 — 具有水平可滚动轴的 scroll views 在 iOS 10 和 iOS 11 中将具有相同的顶部和底部 insets。
Adjusted Content Insets
在 iOS 11 中,UIScrollView 有了新的 adjustedContentInset
属性:
@available(iOS 11.0, *)
open var adjustedContentInset: UIEdgeInsets { get }
contentInset
和 adjustedContentInset
有什么区别?让我们打印当 scroll view 被顶部的导航栏和底部的标签栏覆盖时的两个值:
// iOS 10
print("contentInset: \(scrollView.contentInset)")
// output: contentInset: UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
// iOS 11
print("contentInset: \(scrollView.contentInset)")
print("adjustedContentInset: \(scrollView.adjustedContentInset)")
// output: contentInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
// output: adjustedContentInset: UIEdgeInsets(top: 88.0, left: 0.0, bottom: 83.0, right: 0.0)
如果我们从四面添加 10 点到 contentInset
并再次打印两个值:
scrollView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
// iOS 10
print("contentInset: \(scrollView.contentInset)")
// output: contentInset: UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
// iOS 11
print("contentInset: \(scrollView.contentInset)")
print("adjustedContentInset: \(scrollView.adjustedContentInset)")
// output: contentInset: UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
// output: adjustedContentInset: UIEdgeInsets(top: 98.0, left: 10.0, bottom: 93.0, right: 10.0)
我们会看到在 iOS 11 中,实际的 scroll view content insets 可以从 adjustedContentInset
属性读取,而不是从 contentInset
属性读取。这意味着当你的应用同时支持 iOS 10 和 iOS 11 时,应该为调整 content insets 创建不同的逻辑。
如果你更改 contentInset
或者 content insets 被系统调整,UIScrollView 和 UIScrollViewDelegate 中的适当方法将被调用:
// UIScrollView
@available(iOS 11.0, *)
open func adjustedContentInsetDidChange()
// UIScrollViewDelegate
@available(iOS 11.0, *)
optional func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)
UITableView
UITableView 在 iOS 11 中也有新的 safe area 相关功能。让我们添加一个具有自定义 header 和自定义 cells 的 table view 到 view controller,并将其附加到视图的边缘。
你可以看到 header 和 cell 的 content view frame 在横屏方向下会改变。同时,cell 和 separator frames 不会改变。这是默认行为,可以通过新的 UITableView 的 insetsContentViewsToSafeArea
属性管理:
@available(iOS 11.0, *)
open var insetsContentViewsToSafeArea: Bool
如果你禁用 content view insets:
tableView.insetsContentViewsToSafeArea = false
你会看到现在 header/footer/cell content views frame 等于 header/footer/cell frame。
这意味着在 iOS 11 中,如果 header/footer/cell subviews 添加到 content view,你不需要更改它们的位置,UITableView 会为你完成这项工作。
UICollectionView
和 UITableView 类似,UICollectionView 也有处理 safe area 的方法。当使用 UICollectionViewFlowLayout 时,iOS 11 中新增了 sectionInsetReference
属性:
@available(iOS 11.0, *)
public enum SectionInsetReference : Int {
case fromContentInset
case fromSafeArea
case fromLayoutMargins
}
@available(iOS 11.0, *)
open var sectionInsetReference: UICollectionViewFlowLayout.SectionInsetReference
这个属性用于确定 section insets 的参考点,有三个值:
- fromContentInset — 默认值,section insets 不会受到 safe area 的影响。
- fromSafeArea — section insets 会添加 safe area insets。在这种情况下,实际的 section content insets 将等于 section content insets 加上 safe area insets。
- fromLayoutMargins — section insets 会添加 layout margins。
当在 iPhone X 的横屏模式下使用 grid 布局时,cells 可能会被 notch 覆盖。为了修复这个问题,可以设置 sectionInsetReference
为 .fromSafeArea
,这样就不需要手动添加 safe area insets 到 section content insets。
总结
在 iOS 11 中,Apple 添加了许多有用的工具来处理 safe area。这些工具在以下类中都有体现:
- UIView: 提供
safeAreaInsets
和safeAreaLayoutGuide
属性,允许在视图层次结构的任何地方访问 safe area。 - UIViewController: 添加
additionalSafeAreaInsets
属性,可以扩展 safe area,对测试非常有用。 - UIScrollView: 引入
contentInsetAdjustmentBehavior
和adjustedContentInset
属性,完全改变了 scroll view insets 的调整方式。 - UITableView: 增加
insetsContentViewsToSafeArea
属性,自动调整 content views 以适应 safe area。 - UICollectionView: 通过 UICollectionViewFlowLayout 的
sectionInsetReference
属性,提供对 section insets 如何响应 safe area 的控制。
所有这些新 API 都使得为 iPhone X 和未来的设备适配应用程序变得更加简单和统一。当开发新的 iOS 11+ 应用时,应该充分利用这些 API,以确保应用在所有屏幕尺寸和形状上看起来都很好。