MENU

iOS 中的 Layout

最近读了两本书一本是已经读完的 《 图解密码技术 》,另一本则是正在啃的 《 Programming iOS 10 》,整个阅读过程只能用酣畅淋漓来表达。这两本书在我为进阶而手足无措的时候很好的缓解了我的技术焦虑感。这里会将后者的内容总结出来,而前者的总结则可能需要往后放一下。后者书中的知识整体来说非常的基础,理解起来不存在什么太大的难度(最起码目前为止给我的感觉是这样)。但是这些基础却给我实实在在的提升感,下面我们先来看视图布局部分。

Layout

在日常的编码中我习惯于直接使用代码来控制视图的布局而较少使用 IB。因为在团队协作过程中,我觉得代码能更清晰的表达出逻辑关系。当然两者之间实际并没有优劣之分纯粹只是个人的偏好而已。客户端开发有非常大的工作量都是在处理视图与交互,其中最重要的一部分就是视图布局控制。虽然业界已经存在 MasonrySnapKit 等成熟的类库,但是我还是希望能够看看底层的原理。 正如我一直都坚持:不要为工程中引入不必要的外部变量,另外类库依赖症可能会给你一种错觉而这隐性伤害是非常大的。

因为 iPhone 的尺寸以及设备旋转子视图尺寸的变化等原因,我们可能需要动态的调整视图之间的位置关系来应对这些情况。解决方案大抵如下:

  • Manual layout:父视图每次尺寸发生变化都会触发 layoutSubviews 的调用,所以可以在子类中重载该函数的实现。

  • Autoresizing:该方法很早就已经有了,子视图通过 autoresizingMask 属性控制其自身布局来应对父视图的变化。

  • Autolayout:该方法是iOS 6 之后引入到系统中的,它比上面的方案更为强大。通过约束关系来描述视图的大小、位置还有与其他视图之间的位置关系。

    Manual layout 的使用频率并不是很高,但是它依然能够发挥作用。 Autoresizing 在系统中默认开启的,除非您通过将 superviewautoresizesSubviews 属性设置为 false 或者视图完全使用了 autolayout 策略将其关闭。 Autolayout 是一种可选策略可用于任何视图。实际编程中的布局策略可以采用上述方式进行某种程度的组合。

Autoresizing

作为最早的视图布局控制器,Autoresizing 当初主要设计的初衷就是用于处理设备旋转导致的控件位置冲突。当然其它场景中该方案也能很好的发挥作用。例如:

  • 子视图位于父视图的中央,当父视图缩放的时候子视图需要同步变化。

  • 子视图位于父视图的中央,当父视图缩放的时候子视图保持大小不变。

  • 确定按键位于视图右下角,设备发生旋转之后该按键应该保持在右下角。

Autoresizing 默认策略是视图的大小和间距是不变的,但是可以通过 autoresizingMask 属性将视图的部分间距或尺寸为设置为可拉伸。

let v1 = UIView(frame:CGRect(x:100, y:111, width:132, height:194))
v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)
let v2 = UIView(frame:CGRect(x:0, y:0, width:132, height:10))
v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)
let v3 = UIView(frame:CGRect(
            x:v1.bounds.width-20, y:v1.bounds.height-20, width:20, height:20))
v3.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
self.view.addSubview(v1)
v1.addSubview(v2)
v1.addSubview(v3)

效果图如下:

1

看起来还不错,但是当改变 v1 的大小后就会出问题了:

v1.bounds.size.width += 40
v1.bounds.size.height -= 50

2

这显然不是理想的结果,所以我们可以通过 Autoresizing 来控制布局:

v2.autoresizingMask = .flexibleWidth
v3.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin]

上面代码中 v2 的宽度被设置为可拉伸,并且 v3 的上间距和左间距也被设置为可拉伸。这样我们就能保证在 v1 视图大小发生变化的后 v2、v3 能过做出正确应对 。

Autolayout

Autolayout 作为可选技术方案可仅对部分视图应用。但是 Autolayout 会沿着视图层级向父级进行传递,也就是说当一个子视图采用 Autolayout 后其父视图通用的也会被动的采用 Autolayout 。如果父视图是控制器的根视图的话,那么控制器将会接受到自动布局相关的事件。

Autolayout 技术的核心就是定义视图间的约束关系 ,然后系统的布局机制会自动通过约束关系计算出视图的正确位置和大小。

Constraints

视图布局中的约束其实是 NSLayoutConstraint 类的一个实例,该实例中描述了视图之间的相对位置关系,但是视图必须在同一个层级结构中。NSLayoutConstraint 实例中主要的属性包含:

  • firstItem, firstAttribute, secondItem, secondAttribute:表示约束关系中两个视图和对应的属性。如果是设置 firstAttribute 为绝对值的话那么 secondItem 为 nil 而 secondAttribute 则为 notAnAttribute

  • multiplier, constant 表示 firstAttributesecondAttribute 之间的数学关系,例如:firstAttribute = secondAttribute multiplier + constant*。

  • relation 表示 firstAttributesecondAttribute 的等量关系,上面的例子中使用的是 .equal 。当然我们也可以使用 .lessThanOrEqual.greaterThanOrEqual 这些不等式关系。例如:firstAttribute >= secondAttribute multiplier + constant*

  • priority 定义了约束的采用的优先级取值范围为 1000 ~ 1,当两个约束出现冲突的时候默认会先采用优先级高的约束定义。如果两个冲突的约束定义都是最高优先级,那么此时程序就会报错。该策略在使用纯代码进行布局的时候常用,因为纯代码并不会像 IB 一样有明显的布局冲突提示。

既然约束是与视图的属性相关的,那么我们就有必要看看到底是哪些属性:

  • .width、 .height

  • .top、 .bottom

  • .left、.right、.leading、.trailing

  • .centerX、centerY

  • .firstBaseline、.lastBaseline

上面大部分的属性的含义都很直白,但是有几个还是需要说明下。.leading、.trailing 对应书写习惯的起始和结尾,例如现代汉语这两个属性就对应 .left、.right,在古汉语中则相反。.firstBaseline、.lastBaseline 对应多行文本的 Label 中第一行和最后一行文本的基线。

当我们创建好约束条件之后可以通过以下函数将其添加到视图或者移除:

  • addConstraint(_:)、addConstraints(_:)

  • removeConstraint(_:)、 removeConstraints(_:)

但是这里还存在一个问题:纯代码创建好的约束应该添加到哪个视图嗯? 答案是:约束中涉及的两个视图所在层级结构中最上层的的视图,大多数情况下是 firstItem、secondItem 中二选一。 例如,如果约束指定了视图的绝对宽度,则它属于该视图; 如果它将视图的顶部与其父视图的顶部相关联,则它属于该父视图; 兄弟视图设置对其方式的时候,则它属于共同的父视图。

看起来这些判断还是有些复杂,不过好在 iOS 8 之后苹果提供了一个更便捷的方法来解决这个问题。可以通过 NSLayoutConstraint 的类方法 activate(_:) 来激活这些约束,然后系统会自动将约束添加到正确的视图上。当然你也可以使用 deactivate(_:) 来让某些约束失效从而将其从视图中移除。

结语

相信此文能够让你对布局的基础内容有了大致了解,下一篇将更详细的介绍布局中的约束。