在界面开发的早期,iPhone 设备屏幕尺寸单一,界面开发只需要针对特定屏幕尺寸进行。随着 iPhone 的不断迭代更新,屏幕尺寸越来越多样化,甚至出现了如今的异形屏(iPhone X 系列机型),导致界面开发需要针对不同大小,不同形状的屏幕做适配,单一的布局不再能满足需求。
Auto Layout 通过一组对象间的关系表达式,唯一确定对象的大小和位置信息,即使用户改变应用的窗口大小,旋转设备的方向,接入外接显示器,切换语言导致文本尺寸变化,动态改变字体大小等等,界面元素也依旧可以自适应以上变化,按照预期显示。
本文按照时间顺序,简单介绍了支撑自动布局技术的相关工具,13 年那会儿我还是个孩纸,没有亲身体验过当时的自动布局实现,本文主要是学习过程的一个记录,有关描述如有错误,恳请指正。
iOS7
很久很久以前(2013 年前),iOS7 之前版本中的状态栏和导航栏默认还是不透明的,wantsFullScreenLayout
用于指示是否使用全屏模式布局,默认值是 NO
(那个年代还没有 Swift,现在使用 Swift 是找不到这个属性的)。如果添加一个视图到带有导航栏的视图控制器的根视图中,则默认从导航栏底部开始布局(此结论也是道听途说,因为目前 Xcode 已经不支持运行 iOS7 的模拟器,所以没有亲自验证)。
后来,iOS7 发布了,伴随着苹果设计风格从拟物化到扁平化的巨大转变,导航栏也默认变成了半透明。于是 wantsFullScreenLayout
属性在 iOS7 中被标记为废弃,取而代之的是 edgesForExtendedLayout
和 extendedLayoutIncludesOpaqueBars
属性,以及相关的 automaticallyAdjustsScrollViewInsets
属性。
iOS7 之后,苹果鼓励全屏布局,edgesForExtendedLayout
属性的默认值就是 UIRectEdgeAll
,即布局原点在屏幕左上角,布局默认基于全屏。如下例所示:
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView.init()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.leading,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.leading,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.trailing,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.trailing,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.top,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.top,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.bottom,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.bottom,
multiplier: 1,
constant: 0).isActive = true
}
但是如果我们手动将导航栏设置成不透明的,则布局还是会基于导航栏下边界。如下边示例所示:
override func viewDidLoad() {
super.viewDidLoad()
// 手动将导航栏设置成不透明,布局依旧基于导航栏下边界
self.navigationController?.navigationBar.isTranslucent = false
let redView = UIView.init()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.leading,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.leading,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.trailing,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.trailing,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.top,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.top,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.bottom,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.bottom,
multiplier: 1,
constant: 0).isActive = true
}
我们也可以保持导航栏默认的半透明状态,通过修改 edgesForExtendedLayout
属性,到达相同的目的。值得注意的是,因为导航栏依旧是半透明的,但是其下方没有视图,所以会呈现灰色效果。如下边示例所示:
override func viewDidLoad() {
super.viewDidLoad()
// 不再扩展布局边界
edgesForExtendedLayout = []
let redView = UIView.init()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.leading,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.leading,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.trailing,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.trailing,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.top,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.top,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.bottom,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.bottom,
multiplier: 1,
constant: 0).isActive = true
}
extendedLayoutIncludesOpaqueBars
属性在 Bar 透明时无效,它指示布局是否包含不透明的 Bar。值得注意的是,如果将此属性设置为 true
,虽然布局原点变成了屏幕左上角,但是因为导航栏不透明,所以其下方的视图内容会被遮挡。如图:
automaticallyAdjustsScrollViewInsets
属性根据视图是否显示了 Bar,自动调整 UIScrollView
对象的 contentInset
属性。也就是说 UIScrollView
的内容不会被遮挡,但是滑动的时候却可以滑动到导航栏下方,该属性在 iOS11 中被标记为废弃。
UILayoutSupport
上文通过几个属性,来实现配置布局原点和相关布局规则,实际上苹果还提供了一种更简单的方式实现这种需求,这种思想也为后来的 UILayoutGuide
、UILayoutAnchor
、UIStackView
以及安全区域奠定了基础。那就是 UILayoutSupport
协议。
UILayoutSupport
协议也是 iOS7 中引入的,用于解决状态栏、导航栏对视图元素的遮挡问题。UIViewController
的 topLayoutGuide
和 bottomLayoutGuide
都是遵循 UILayoutSupport
协议的属性。UILayoutSupport
协议当时只定义了一个 length
属性,指示导航栏或者 TabBar 的高度,通过基于 topLayoutGuide
或者 bottomLayoutGuide
布局,就可以避免视图被遮挡。下边的例子通过使用 topLayoutGuide
属性,实现了上文中需求。值得注意的是,topLayoutGuide
属性只有在视图被加载到视图层级之后才有准确的值,所以上边的代码从 viewDidLoad
方法中移到了 viewDidAppear
方法中。如下边示例所示:
override func viewDidAppear(_ animated: Bool) {
let redView = UIView.init()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.leading,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.leading,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.trailing,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.trailing,
multiplier: 1,
constant: 0).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.top,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.top,
multiplier: 1,
// self.topLayoutGuide.length 即是 Bar 视图的高度
constant: self.topLayoutGuide.length).isActive = true
NSLayoutConstraint.init(item: redView,
attribute: NSLayoutConstraint.Attribute.bottom,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.bottom,
multiplier: 1,
constant: 0).isActive = true
}
iOS9
2014 年,随着 iOS9 的发布,苹果从 UILayoutSupport
协议的设计思想上,进一步扩展出了 一系列针对自动布局的更高级和更好用的工具,包括 UILayoutGuide
、UILayoutAnchor
、UIStackView
等等。
在这之前,我们先看看约束到底是什么?
NSLayoutConstraint
一条约束,就是一个描述两个视图对象间位置或者大小关系的方程:
item1.attribute1 = multiplier × item2.attribute2 + constant
约束的默认优先级 1000 表示必须满足此约束,当无法满足所有约束时,按照优先级尽可能满足。
实际开发过程中,我们很少直接通过 NSLayoutConstraint
的初始化方法创建约束,因为其繁琐的语法实现实在反人类。。。
// Creating constraints using NSLayoutConstraint
// Creating constraints using NSLayoutConstraint
NSLayoutConstraint(item: subview,
attribute: .leading,
relatedBy: .equal,
toItem: view,
attribute: .leadingMargin,
multiplier: 1.0,
constant: 0.0).isActive = true
NSLayoutConstraint(item: subview,
attribute: .trailing,
relatedBy: .equal,
toItem: view,
attribute: .trailingMargin,
multiplier: 1.0,
constant: 0.0).isActive = true
NSLayoutAnchor
布局锚点 NSLayoutAnchor
是一个提供简单 API 用于创建视图对象间约束的工具类,从根本上解决了 NSLayoutConstraint
反人类的初始化方法问题。不过此类从 iOS9 开始引入,对于需要维护 iOS8 的项目,呵呵哒。。。
// Creating the same constraints using Layout Anchors
let margins = view.layoutMarginsGuide
subview.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
subview.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
UIView
提供了如下布局锚点:
extension UIView {
/* Constraint creation conveniences. See NSLayoutAnchor.h for details.
*/
@available(iOS 9.0, *)
open var leadingAnchor: NSLayoutXAxisAnchor { get }
@available(iOS 9.0, *)
open var trailingAnchor: NSLayoutXAxisAnchor { get }
@available(iOS 9.0, *)
open var leftAnchor: NSLayoutXAxisAnchor { get }
@available(iOS 9.0, *)
open var rightAnchor: NSLayoutXAxisAnchor { get }
@available(iOS 9.0, *)
open var topAnchor: NSLayoutYAxisAnchor { get }
@available(iOS 9.0, *)
open var bottomAnchor: NSLayoutYAxisAnchor { get }
@available(iOS 9.0, *)
open var widthAnchor: NSLayoutDimension { get }
@available(iOS 9.0, *)
open var heightAnchor: NSLayoutDimension { get }
@available(iOS 9.0, *)
open var centerXAnchor: NSLayoutXAxisAnchor { get }
@available(iOS 9.0, *)
open var centerYAnchor: NSLayoutYAxisAnchor { get }
@available(iOS 9.0, *)
open var firstBaselineAnchor: NSLayoutYAxisAnchor { get }
@available(iOS 9.0, *)
open var lastBaselineAnchor: NSLayoutYAxisAnchor { get }
}
在上述布局锚点中,leadingAnchor
、trailingAnchor
分别等价于 leftAnchor
、rightAnchor
,都可以用于表示“左右”,但是他们并不能混用,下面的约束将导致运行时崩溃:
v.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leftAnchor).isActive = true
UIView
并没有直接提供用于参照视图边距布局的锚点,作为替代,我们可以使用 layoutMarginsGuide
属性来作为布局的参照,NSLayoutGuide
类同样提供了上述布局锚点。
使用 NSLayoutAnchor
实现上文中的布局:
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView.init()
redView.backgroundColor = UIColor.red
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
redView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
redView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
redView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
redView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
可见相对于直接使用 NSLayoutConstraint
的接口,NSLayoutAnchor
确实简洁了很多。
UILayoutGuide
UILayoutGuide
用于替代传统布局中那些不需要显示出来,却又需要借助他们来实现完整布局的视图。如下图:
需求是把橙色和绿色两个视图,整体居中显示。传统的布局方法可能是将两个视图放在一个容器视图中,然后将容器视图居中。这种方法会引入一个用户看不到的视图来辅助实现布局,但这个辅助视图实实在在的存在于视图层级中,存在额外的开销,会正常接收甚至拦截用户事件,存在引入 bug 的风险。
UILayoutGuide
应运而生。UILayoutGuide
对象仅仅用于在视图层级结构中定义一个虚拟的矩形区域,来辅助布局,相对于引入一个辅助视图,这种方案更快更安全。和 UIView
类似的,UILayoutGuide
同样提供了用于布局的各种锚点。使用 UILayoutGuide
实现上述需求的代码如下:
let layoutGuide = UILayoutGuide.init()
view.addLayoutGuide(layoutGuide)
layoutGuide.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
layoutGuide.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let orangeView = UIView.init()
orangeView.backgroundColor = UIColor.orange
view.addSubview(orangeView)
orangeView.translatesAutoresizingMaskIntoConstraints = false
orangeView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
orangeView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
orangeView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
orangeView.widthAnchor.constraint(equalToConstant: 164).isActive = true
orangeView.heightAnchor.constraint(equalToConstant: 128).isActive = true
let greenView = UIView.init()
greenView.backgroundColor = UIColor.green
view.addSubview(greenView)
greenView.translatesAutoresizingMaskIntoConstraints = false
greenView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
greenView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
greenView.leadingAnchor.constraint(equalTo: orangeView.trailingAnchor).isActive = true
greenView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
greenView.widthAnchor.constraint(equalToConstant: 255).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 128).isActive = true
UIStackView
使用 Auto Layout 最简单的方式,就是使用 UIStackView
对象。 UIStackView
是一个继承自 UIView
,但本身不提供任何可视效果的视图对象,通过 Auto Layout 技术,UIStackView
对其 arrangedSubviews
属性中的一组视图做横向或者纵向的布局。UIStackView
支持嵌套,可以实现复杂的布局。
UIStackView
通过四个核心的属性来配置子视图的布局:
-
axis
:指示UIStackView
对其管理的视图做横向(NSLayoutConstraint.Axis.horizontal
)或者纵向(NSLayoutConstraint.Axis.vertical
)布局。 -
distribution
:指示UIStackView
在axis
方向上的布局规则,默认值为UIStackView.Distribution.fill
,其取值有如下几种:
a.UIStackView.Distribution.fill
:最大程度利用axis
方向的空间显示内容,如果内容太大,则根据各元素的抗压缩优先级进行压缩,如果内容太小,则根据各元素的抗拉伸优先级进行拉伸,如果有冲突,则根据各元素在arrangedSubviews
中的顺序作为优先级进行调整。
b.UIStackView.Distribution.fillEqually
:将axis
方向的空间等分,应用在每个视图之上。
c.UIStackView.Distribution.fillProportionally
:按照各元素的大小比例,分配axis
方向的空间,最大程度显示内容。
d.UIStackView.Distribution.equalSpacing
:最大程度利用axis
方向的空间显示内容,如果内容太大,则根据各元素的抗压缩优先级进行压缩,如果内容太小,则将剩余空间等分,应用于各元素之间。
e.UIStackView.Distribution.equalCentering
:最大程度利用axis
方向的空间显示内容,在保证满足spacing
属性的前提下,使各元素中心之间的距离相等,如果内容太大,则根据各元素的抗压缩优先级进行压缩,如果有冲突,则根据各元素在arrangedSubviews
中的顺序作为优先级进行调整。
-
alignment
:指示UIStackView
在垂直于axis
的方向上的布局规则,默认值为UIStackView.Alignment.fill
,其取值有如下几种:
a.UIStackView.Alignment.fill
:填充满垂直于轴向的空间。
b.UIStackView.Alignment.leading
:适用于纵向布局的 Stack View,各元素左对齐。
c.UIStackView.Alignment.top
:适用于横向布局的 Stack View,各元素上对齐。
d.UIStackView.Alignment.firstBaseline
:仅适用于横向布局的 Stack View,各元素参考其firstBaseline
对齐。
e.UIStackView.Alignment.center
:各元素居中对齐。
f.UIStackView.Alignment.trailing
:适用于纵向布局的 Stack View,各元素右对齐。
g.UIStackView.Alignment.bottom
:适用于横向布局的 Stack View,各元素下对齐。
h.UIStackView.Alignment.lastBaseline
:仅适用于横向布局的 Stack View,各元素参考其lastBaseline
对齐。
-
spaceing
:指示axis
方向上元素间的间距,默认值为 0,设置成负数表示允许元素重叠,对于UIStackView.Distribution.fillProportionally
来说,这是一个严格的距离约束,对于UIStackView.Distribution.equalSpacing
和UIStackView.Distribution.equalCentering
来说,这是最小需要满足的距离。
除了spacing
属性指定各元素之间的间距,还可以通过setCustomSpacing(_:after:)
方法对特定元素之间的间距做调整,
UIStackView
的 arrangedSubviews
属性和 subviews
属性满足如下一致性规则:
1. 将视图添加到 arrangedSubviews
中,也会自动将其添加到 subviews
中。
2. 将视图添加到 subviews
中,不会自动将其添加到 arrangedSubviews
中。
3. 将视图从 arrangedSubviews
中移除,并不会自动从 subviews
中移除,仅仅是不再管理其大小和位置。
4. 将视图从 subviews
中移除,会自动从 arrangedSubviews
中移除。
5. arrangedSubviews
和 subviews
内部的顺序相互独立,前者定义显示顺序(从左到右,自上而下),后者定义 x 轴上的顺序。
为了保证 arrangedSubviews
属性和 subviews
属性的一致性规则,我们不能直接操作 UIStackView
的 arrangedSubviews
属性,但有一些列方法可以对其进行操作:addArrangedSubview(_:)
添加视图,insertArrangedSubview(_:at:)
将视图添加到特定索引的位置,removeArrangedSubview(_:)
移除特定的视图。
UIStackView
对子视图的布局默认是相对自身边界的,通过修改 isLayoutMarginsRelativeArrangement
属性可以改变这一行为。
通过这个 demo,可以比较全面和直观地看到 UIStackView
各个属性的变化在视觉效果上的表现:
值得一提的是,这个 demo 中所有的视图控件均使用 UIStackView
实现,写完之后,笔者认为 UIStackView
更适合在 IB 环境中使用,特别是对于复杂嵌套的场景,使用代码实现太过于繁琐了。
Auto Layout 参照
子视图在基于父视图布局时,有几种不同的参照:
- 基于父视图
bounds
布局:
let containerView = UIView.init(frame: view.frame)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = UIColor.red
view.addSubview(containerView)
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
let v = UIView.init()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.white
containerView.addSubview(v)
v.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
v.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
v.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
v.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
- 基于父视图
layoutMarginsGuide
布局:
let containerView = UIView.init(frame: view.frame)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = UIColor.red
view.addSubview(containerView)
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
let v = UIView.init()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.white
containerView.addSubview(v)
v.leftAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leftAnchor).isActive = true
v.rightAnchor.constraint(equalTo: containerView.layoutMarginsGuide.rightAnchor).isActive = true
v.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor).isActive = true
v.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true
两种参照通过 IB 中的 Constrain to margins
勾选项切换:
我们可以通过修改 directionalLayoutMargins
属性来改变视图默认的边距值,UIKit
为控制器的根视图设置了最小边距值 systemMinimumLayoutMargins
以保证视图内容可以正确显示,除非我们将 viewRespectsSystemMinimumLayoutMargins
属性设置为 false
,否则当 directionalLayoutMargins
中的值小于 systemMinimumLayoutMargins
中的值是,会使用后者。另外,视图的实际边距值,是通过综合 directionalLayoutMargins
、insetsLayoutMarginsFromSafeArea
、preservesSuperviewLayoutMargins
属性后得到的。
- 基于父视图安全区域布局:安全区域是 iOS11 中引入的概念,是一个
UILayoutGuide
对象,该属性定义的是一块儿我们的自定义视图不会被其他视图遮挡的虚拟矩形区域。如图所示:
每个UIView
对象,都有自己的safeAreaLayoutGuide
属性,基于它的布局,可以避免我们的视图被其他视图遮挡。与之对应的frame
布局版本,是safeAreaInsets
属性。
对于控制器的根视图,我们可以通过修改 additionalSafeAreaInsets
属性,来扩展安全区域。如图所示:
override func viewDidAppear(_ animated: Bool) {
var newSafeArea = UIEdgeInsets()
// Adjust the safe area to accommodate
// the width of the side view.
if let sideViewWidth = sideView?.bounds.size.width {
newSafeArea.right += sideViewWidth
}
// Adjust the safe area to accommodate
// the height of the bottom view.
if let bottomViewHeight = bottomView?.bounds.size.height {
newSafeArea.bottom += bottomViewHeight
}
// Adjust the safe area insets of the
// embedded child view controller.
let child = self.childViewControllers[0]
child.additionalSafeAreaInsets = newSafeArea
}
需要注意,在视图被添加到视图层级之前,其安全区域是不准确的,因此对安全区域的修改需要在视图被添加到视图层级之后。另外,我们还可以通过 safeAreaInsetsDidChange
方法来响应安全区域的改变。
参考
- Auto Layout Guide
- View Layout
- 全屏布局(fullScreenLayout)那些事
- iOS开发-LayoutGuide的前世今生(从top/bottom LayoutGuide到Safe Area)