View Layout

作者: 1fea32c0500e | 来源:发表于2019-05-01 09:22 被阅读27次

    在界面开发的早期,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 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 }
    }
    

    在上述布局锚点中,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 = 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 通过四个核心的属性来配置子视图的布局:

    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 = 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
    
    1. 基于父视图 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 中的值是,会使用后者。另外,视图的实际边距值,是通过综合 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)

    相关文章

      网友评论

        本文标题:View Layout

        本文链接:https://www.haomeiwen.com/subject/rleinqtx.html