View Layout

作者 : 开心源码 本文共17036个字,预计阅读时间需要43分钟 发布时间: 2022-05-12 共223人阅读

在界面开发的早期,iPhone 设施屏幕尺寸单一,界面开发只要要针对特定屏幕尺寸进行。随着 iPhone 的不断迭代升级,屏幕尺寸越来越多样化,甚至出现了如今的异形屏(iPhone X 系列机型),导致界面开发需要针对不同大小,不同形状的屏幕做适配,单一的布局不再能满足需求。

Auto Layout 通过一组对象间的关系表达式,唯一确定对象的大小和位置信息,即便客户改变应用的窗口大小,旋转设施的方向,接入外接显示器,切换语言导致文本尺寸变化,动态改变字体大小等等,界面元素也仍旧可以自适应以上变化,按照预期显示。

本文按照时间顺序,简单详情了支撑自动布局技术的相关工具,13 年那会儿我还是个孩纸,没有亲自体验过当时的自动布局实现,本文主要是学习过程的一个记录,有关形容如有错误,恳请指正。

iOS7

很久很久以前(2013 年前),iOS7 之前版本中的状态栏和导航栏默认还是不透明的,wantsFullScreenLayout 用于指示能否使用全屏模式布局,默认值是 NO(那个年代还没有 Swift,现在使用 Swift 是找不到这个属性的)。假如增加一个视图到带有导航栏的视图控制器的根视图中,则默认从导航栏底部开始布局(此结论也是道听途说,由于目前 Xcode 已经不支持运行 iOS7 的模拟器,所以没有亲身验证)。

后来,iOS7 发布了,伴随着苹果设计风格从拟物化到扁平化的巨大转变,导航栏也默认变成了半透明。于是 wantsFullScreenLayout 属性在 iOS7 中被标记为废弃,取而代之的是 edgesForExtendedLayoutextendedLayoutIncludesOpaqueBars 属性,以及相关的 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

上文通过几个属性,来实现配置布局原点和相关布局规则,实际上苹果还提供了一种更简单的方式实现这种需求,这种思想也为后来的 UILayoutGuideUILayoutAnchorUIStackView 以及安全区域奠定了基础。那就是 UILayoutSupport 协议。

UILayoutSupport 协议也是 iOS7 中引入的,用于处理状态栏、导航栏对视图元素的遮挡问题。UIViewControllertopLayoutGuidebottomLayoutGuide 都是遵循 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 协议的设计思想上,进一步扩展出了 一系列针对自动布局的更高级和更好用的工具,包括 UILayoutGuideUILayoutAnchorUIStackView 等等。

在这之前,我们先看看束缚究竟是什么?

NSLayoutConstraint

一条束缚,就是一个形容两个视图对象间位置或者者大小关系的方程:
item1.attribute1 = multiplier × item2.attribute2 + constant
束缚的默认优先级 1000 表示必需满足此束缚,当无法满足所有束缚时,按照优先级尽可能满足。
实际开发过程中,我们很少直接通过 NSLayoutConstraint 的初始化方法创立束缚,由于其繁琐的语法实现实在反人类。。。

// Creating constraints using NSLayoutConstraint// Creating constraints using NSLayoutConstraintNSLayoutConstraint(item: subview,                   attribute: .leading,                   relatedBy: .equal,                   toItem: view,                   attribute: .leadingMargin,                   multiplier: 1.0,                   constant: 0.0).isActive = trueNSLayoutConstraint(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 Anchorslet margins = view.layoutMarginsGuidesubview.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = truesubview.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 }}

在上述布局锚点中,leadingAnchortrailingAnchor 分别等价于 leftAnchorrightAnchor,都可以用于表示“左右”,但是他们并不能混用,下面的束缚将导致运行时崩溃:
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 = truelayoutGuide.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true        let orangeView = UIView.init()orangeView.backgroundColor = UIColor.orangeview.addSubview(orangeView)orangeView.translatesAutoresizingMaskIntoConstraints = falseorangeView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = trueorangeView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = trueorangeView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = trueorangeView.widthAnchor.constraint(equalToConstant: 164).isActive = trueorangeView.heightAnchor.constraint(equalToConstant: 128).isActive = true        let greenView = UIView.init()greenView.backgroundColor = UIColor.greenview.addSubview(greenView)greenView.translatesAutoresizingMaskIntoConstraints = falsegreenView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = truegreenView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = truegreenView.leadingAnchor.constraint(equalTo: orangeView.trailingAnchor).isActive = truegreenView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = truegreenView.widthAnchor.constraint(equalToConstant: 255).isActive = truegreenView.heightAnchor.constraint(equalToConstant: 128).isActive = true

UIStackView

使用 Auto Layout 最简单的方式,就是使用 UIStackView 对象。 UIStackView 是一个继承自 UIView,但本身不提供任何可视效果的视图对象,通过 Auto Layout 技术,UIStackView 对其 arrangedSubviews 属性中的一组视图做横向或者者纵向的布局。UIStackView 支持嵌套,可以实现复杂的布局。

UIStackView 通过四个核心的属性来配置子视图的布局:

  1. axis:指示 UIStackView 对其管理的视图做横向(NSLayoutConstraint.Axis.horizontal)或者者纵向(NSLayoutConstraint.Axis.vertical)布局。
  2. distribution:指示 UIStackViewaxis 方向上的布局规则,默认值为 UIStackView.Distribution.fill,其取值有如下几种:
    a. UIStackView.Distribution.fill:最大程度利用 axis 方向的空间显示内容,假如内容太大,则根据各元素的抗压缩优先级进行压缩,假如内容太小,则根据各元素的抗拉伸优先级进行拉伸,假如有冲突,则根据各元素在 arrangedSubviews 中的顺序作为优先级进行调整。
    UIStackView.Distribution.fill
    b. UIStackView.Distribution.fillEqually:将 axis 方向的空间等分,应用在每个视图之上。
    UIStackView.Distribution.fillEqually
    c. UIStackView.Distribution.fillProportionally:按照各元素的大小比例,分配 axis 方向的空间,最大程度显示内容。
    UIStackView.Distribution.fillProportionally
    d. UIStackView.Distribution.equalSpacing:最大程度利用 axis 方向的空间显示内容,假如内容太大,则根据各元素的抗压缩优先级进行压缩,假如内容太小,则将剩余空间等分,应用于各元素之间。
    UIStackView.Distribution.equalSpacing
    e. UIStackView.Distribution.equalCentering:最大程度利用 axis 方向的空间显示内容,在保证满足 spacing 属性的前提下,使各元素中心之间的距离相等,假如内容太大,则根据各元素的抗压缩优先级进行压缩,假如有冲突,则根据各元素在 arrangedSubviews 中的顺序作为优先级进行调整。
    UIStackView.Distribution.equalCentering
  3. alignment:指示 UIStackView 在垂直于 axis 的方向上的布局规则,默认值为 UIStackView.Alignment.fill,其取值有如下几种:
    a. UIStackView.Alignment.fill:填充满垂直于轴向的空间。
    UIStackView.Alignment.fill
    b. UIStackView.Alignment.leading:适用于纵向布局的 Stack View,各元素左对齐。
    UIStackView.Alignment.leading
    c. UIStackView.Alignment.top:适用于横向布局的 Stack View,各元素上对齐。
    UIStackView.Alignment.top
    d. UIStackView.Alignment.firstBaseline:仅适用于横向布局的 Stack View,各元素参考其 firstBaseline 对齐。
    UIStackView.Alignment.firstBaseline
    e. UIStackView.Alignment.center:各元素居中对齐。
    UIStackView.Alignment.center
    f. UIStackView.Alignment.trailing:适用于纵向布局的 Stack View,各元素右对齐。
    UIStackView.Alignment.trailing
    g. UIStackView.Alignment.bottom:适用于横向布局的 Stack View,各元素下对齐。
    UIStackView.Alignment.bottom
    h. UIStackView.Alignment.lastBaseline:仅适用于横向布局的 Stack View,各元素参考其 lastBaseline 对齐。
    UIStackView.Alignment.lastBaseline
  4. spaceing:指示 axis 方向上元素间的间距,默认值为 0,设置成负数表示允许元素重叠,对于 UIStackView.Distribution.fillProportionally 来说,这是一个严格的距离束缚,对于 UIStackView.Distribution.equalSpacingUIStackView.Distribution.equalCentering 来说,这是最小需要满足的距离。
    除了 spacing 属性指定各元素之间的间距,还可以通过 setCustomSpacing(_:after:) 方法对特定元素之间的间距做调整,

UIStackViewarrangedSubviews 属性和 subviews 属性满足如下一致性规则:
1. 将视图增加到 arrangedSubviews 中,也会自动将其增加到 subviews 中。
2. 将视图增加到 subviews 中,不会自动将其增加到 arrangedSubviews 中。
3. 将视图从 arrangedSubviews 中移除,并不会自动从 subviews 中移除,仅仅是不再管理其大小和位置。
4. 将视图从 subviews 中移除,会自动从 arrangedSubviews 中移除。
5. arrangedSubviewssubviews 内部的顺序相互独立,前者定义显示顺序(从左到右,自上而下),后者定义 x 轴上的顺序。

为了保证 arrangedSubviews 属性和 subviews 属性的一致性规则,我们不能直接操作 UIStackViewarrangedSubviews 属性,但有少量列方法可以对其进行操作:addArrangedSubview(_:) 增加视图,insertArrangedSubview(_:at:) 将视图增加到特定索引的位置,removeArrangedSubview(_:) 移除特定的视图。

UIStackView 对子视图的布局默认是相对自身边界的,通过修改 isLayoutMarginsRelativeArrangement 属性可以改变这一行为。

通过这个 demo,可以比较全面和直观地看到 UIStackView 各个属性的变化在视觉效果上的体现:

值得一提的是,这个 demo 中所有的视图控件均使用 UIStackView 实现,写完之后,笔者认为 UIStackView 更适合在 IB 环境中使用,特别是对于复杂嵌套的场景,使用代码实现太过于繁琐了。

Auto Layout 参照

子视图在基于父视图布局时,有几种不同的参照:

  1. 基于父视图 bounds 布局:
let containerView = UIView.init(frame: view.frame)containerView.translatesAutoresizingMaskIntoConstraints = falsecontainerView.backgroundColor = UIColor.redview.addSubview(containerView)containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = truecontainerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = truecontainerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = truecontainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true        let v = UIView.init()v.translatesAutoresizingMaskIntoConstraints = falsev.backgroundColor = UIColor.whitecontainerView.addSubview(v)v.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = truev.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = truev.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = truev.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
  1. 基于父视图 layoutMarginsGuide 布局:
let containerView = UIView.init(frame: view.frame)containerView.translatesAutoresizingMaskIntoConstraints = falsecontainerView.backgroundColor = UIColor.redview.addSubview(containerView)containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = truecontainerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = truecontainerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = truecontainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true        let v = UIView.init()v.translatesAutoresizingMaskIntoConstraints = falsev.backgroundColor = UIColor.whitecontainerView.addSubview(v)v.leftAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leftAnchor).isActive = truev.rightAnchor.constraint(equalTo: containerView.layoutMarginsGuide.rightAnchor).isActive = truev.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor).isActive = truev.bottomAnchor.constraint(equalTo: containerView.layoutMarginsGuide.bottomAnchor).isActive = true

两种参照通过 IB 中的 Constrain to margins 勾选项切换:

我们可以通过修改 directionalLayoutMargins 属性来改变视图默认的边距值,UIKit 为控制器的根视图设置了最小边距值 systemMinimumLayoutMargins 以保证视图内容可以正确显示,除非我们将 viewRespectsSystemMinimumLayoutMargins 属性设置为 false,否则当 directionalLayoutMargins 中的值小于 systemMinimumLayoutMargins 中的值是,会使用后者。另外,视图的实际边距值,是通过综合 directionalLayoutMarginsinsetsLayoutMarginsFromSafeAreapreservesSuperviewLayoutMargins 属性后得到的。

  1. 基于父视图安全区域布局:安全区域是 iOS11 中引入的概念,是一个 UILayoutGuide 对象,该属性定义的是一块儿我们的自己设置视图不会被其余视图遮挡的虚拟矩形区域。如图所示:

    每个 UIView 对象,都有自己的 safeAreaLayoutGuide 属性,基于它的布局,可以避免我们的视图被其余视图遮挡。与之对应的 frame 布局版本,是 safeAreaInsets 属性。

对于控制器的根视图,我们可以通过修改 additionalSafeAreaInsets 属性,来扩展安全区域。如图所示:

image.png

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 方法来响应安全区域的改变。

参考

  1. Auto Layout Guide
  2. View Layout
  3. 全屏布局(fullScreenLayout)那些事
  4. iOS开发-LayoutGuide的前世今生(从top/bottom LayoutGuide到Safe Area)

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » View Layout

发表回复