前言
首先,我们通过下面这张图片引出今天的主角
大家看到了什么,是爱吗?
不,这不是爱,不是爱,是满满的‘愁绪’?
此刻,面对这一堆约束,或许你的内心是淡然的,那么,如果情况是这堆约束里面存在一连串的Conflicting Constraints呢,是否感到崩溃。虽然对于这些警告和冲突,系统会有相应的提示,但错综复杂的约束会让你依然无从下手。一般情况下,对于修复类似稍微复杂的自动布局环境,如果你脑海里没有清晰的约束结构,个人建议还是重新来过,这样一般情况下会比你扯着头皮去解析来得省时省力得多。当然,如果使用UIStackView,我们就不必再为这成串的约束苦恼了,UIStackView能够自动管理它内部子控件的约束,从而会为我们省去大部分的手动约束。
UIStackView介绍
UIStackView 是 iOS 9 中新增的一个控件,它继承于UIView,用来管理一行或一列视图的布局。UIStackView新增了几个属性,这些属性就是子视图布局规则。一旦UIStackView的这些属性发生变化,它的arrangedSubviews就会按照规则重新排布。只要我们掌握这些规则,就可以管理视图布局了。如果能再稍加灵活运用,有时候我们甚至能轻松实现一些精妙布局。
这里说明下UIStackView的arrangedSubviews数组属性,它是subViews数组的子集合,我觉得有必要给大家重复下官方的几条说明:
- 无论何时 stackView添加了一个视图到它的 arrangedSubviews 数组,其也将把这个视图作为子视图添加,如果还未添加的话。
- 无论何时一个子视图从 stackVierw中被移除,那么 stackView也会将其从 arrangedSubviews 数组中移除。
- 从 arrangedSubviews 移除一个视图并不会将其作为子视图移除。stack 视图将不再管理该视图的尺寸和位置,但是该视图仍将是视图结构的一部分,并且当其可见的情况下仍会被渲染到屏幕上。
以下是官方对StackView的布局属性解释:
- axis(轴向) 属性决定了 stack 的朝向,只有垂直或水平;
- distribution(分布) 属性决定了其管理的视图在沿着其轴向上的布局;
- alignment(对齐) 属性决定了其管理的视图在垂直于其轴向上的布局;
- spacing(空隙) 属性决定了其管理的视图间的最小间隙;
- layoutMarginsRelativeArrangement 属性决定了 stack 视图平铺其管理的视图时是否要参照它的布局边距;
- baselineRelativeArrangement 属性决定了 stack 视图平铺其管理的视图时是否要参照它的布局边距;
下图是官方对alignment和distribution以及Spacing三个属性的图解:
这里附上我制作的一个 示例Demo,里面主要使用UIStackView和sizeclass完成了Demo主页与StackView属性展示的iPhone横竖屏的适配,小伙伴们可以通过在属性界面调整StackView的属性来观察属性变化对StackView的布局影响。
UIStackViewDistribution和UIStackViewalignment
axis、spacing、distribution和alignment是比较重要的4个属性,他们都能给布局带来明显的变化。axis和spacing属性作用单一,通过属性解释或者通过视图简单观察我们就能理解他们的作用。distribution和alignment这两个属性相对而言更具灵活性,也更具有难度,尤其是二者的结合使用。
我们先来看一下这两个属性的定义
public enum UIStackViewDistribution : Int {
case fill
case fillEqually
case fillProportionally
case equalSpacing
case equalCentering
}
public enum UIStackViewAlignment : Int {
case fill
case leading
public static var top: UIStackViewAlignment { get }
case firstBaseline
case center
case trailing
public static var bottom: UIStackViewAlignment { get }
case lastBaseline
}
这里我介绍一下distribution的各类模式(图片来自官方文档):
- UIStackViewDistributionFill
将arrangedSubviews填充满整个StackView,他们之间的间隙等于spacing大小。如果减去所有的spacing,所有arrangedSubview的固有尺寸(intrinsicContentSize)之和不能填满StackView,那么就按照Hugging的优先级将其拉伸。反之,如果超出StackView的尺寸则按CompressionResistance的优先级压缩。如果优先级相同,就按排列顺序来拉伸或压缩。
- UIStackViewDistributionFillEqually
每个arrangedSubview沿axis方向的长度相等,等于StackView沿axis长度减去spacing之和除以arrangedSubviews个数。
- UIStackViewDistributionFillProportionally
根据arrangedSubview的intrinsicContentSize,将StackView沿axis方向的长度减去spacing之和按比例分配给arrangedSubviews。
- UIStackViewDistributionEqualSpacing
先按arrangedSubviews的intrinsicContentSize布局,然后余下的空间均分为spacing,如果spacing小于StackView设置的spacing,则按照CompressionResistance的优先级来压缩arrangedSubviews。
- UIStackViewDistributionEqualCentering
令arrangedSubviews的中心点之间的距离相等,且spacing大于等于StackView设置的spacing(每两个arrangedSubview之间的spacing可能不相等)。如果spacing小于StackView设置的spacing,则按照CompressionResistance的优先级来压缩arrangedSubviews。
对于alignment属性,它决定的是垂直axis方向的对齐方式,从命名上我们也能看出个大概。有两点我们需要了解一下:
1、Top与Leading模式相同,Bottom与Trailing模式相同,从UIStackViewAlignment的定义中我们也能隐约猜出。
2、FirstBaseline和LastBaseline这两种模式是让arrangedSubviews分别按照firstBaseline和lastBaseline对齐。只能出现在axis为水平的StackView中。其实这两种alignment在垂直StackView中也是可以设置的,只是显示时有些怪异罢了)
storyboard添加StackView
方法1:从对象库中拖拽UIStackView到storyboard中,然后往内部扔控件(UIView或其子类)就可以了。
方法2:选择storyboard中的控件,可以用“ommand键 + 单击”进行多选,然后点击下方的stack按钮,这样选中的控件就会被放入一个StackView中
StackView使用技巧
1、嵌套
只要嵌套好UIStackView,就可以用很少的约束达到自动布局界面的目的。
2、增量排版
由于我们StackView中我们常用的alignment模式为Fill,因此会经常出现arrangedSubView被拉伸变形的情况,也许有人会想到给控件添加宽高约束,但这会与StackView自动生成的约束突出,并不能达到目的。这里我们可以将这个arrangedSubView装进一个StackView中,然后添加透明的UIView,通过添加UIView的宽度或者高度约束从而达到约束arrangedSubView的目的。
3、结合sizeClass
查看SatckView的Attributes Inspector,我们会发现StackView的几个主要属性都是可以设置sizeclass模式的,这对我们的屏幕适配将会大有助益。再加上约束的sizeClass,灵活性可以想象。
布局实践
下面我就带大家感受下Demo中是如何利用这三点技巧的。
首先附上Reveal中Demo首页视图结构层次:
在这个页面中,我粗略计算了下大约一共有14条约束,这其中还包括了横屏的适配,视图布局的优化。如果是粗糙布局,我们甚至可以用小于6条的约束完成布局。那么,问题来了,如果这些视图是纯粹用自动布局排版的,请问约束数目几何?
为了方便说明,我将Demo中的主页视图分为5个区域:
制作Demo时我采用的是整体到局部的思路,即先限定容器(最外层StackView)的大小,然后添加子容器(区域StackView),根据需求添加相应的内容以及判断是否还需要子容器。就拿区域1来说,它的结构是这样的:
这里添加UIView就是采用了增量排版,因为如果label的文字直抵屏幕边距影响美观,所以我让UIView为其创造边距。再如区域5的泊学logo,为了不使其被拉伸,我在两边添加了两个UIView,从而达到约束其宽度的目的。
针对横屏适配,我主要采用了以下几条策略
1、设置不同sizeClass模式下区域1的高度
2、区域2由三个模块组成(button+两个StackVIew),两个StackView宽度相同,wAny | hAny 模式下为0。在wAny | hCompact模式下添加Button的宽度约束,StackView就拥有了宽度。
3、通过高度约束,在wAny | hCompact模式下隐藏区域3,因为我们将它放在区域2显示了。
4、调整不同sizeClass下的spacing(横屏下高度资源不容浪费)
最后我们的视图在横屏下是这样的
StackView带给我们的好
大量减少自定义约束以及增强灵活度:曾经的约束就像用绳子将一个个控件捆在了一起,而现在的StackView更像一个个盒子将控件装了起来,我们可以用大盒子装小盒子,也可以将这个盒子的东西转移到另一个盒子。盒子的存在使得我们只要掌握最大的那个盒子,盒子内的器件也终将被我们掌握,毕竟盒子内的器件无论位置怎么变动,它始终存在于盒子之中。
层次关系: UIStackView的嵌套以及对arrangedSubViews的排列使得界面控件之间的结构层次更为清晰。代码布局:有了StackView,代码添加自动布局明显也方便了许多,无需像以前那样为每一条约束写一串语句,只要把视图分层包装好,通过设置UIStackView的属性,就可以自动布局整个界面了。总结
最后,我们在使用stackView的时候,尽量通过设置stackView的属性来完成布局,尽可能的减少内部子控件的约束,这样可以让视图更具灵活性,也避免了许多约束冲突,增强可维护性。灵活利用StackView的嵌套,再添加一点我们的小思维,让视图如同被赋予生命一般跃动起来吧!
stack确实给我们的开发带来种种益处,我们没有理由不去学习掌握它。而且如果大家想在iOS9以下系统使用UIStackView也并不是没有可能,sunny大神和他的同事利用运行时制作的 FDStackView 这个框架让你感觉仿佛可以在iOS6+环境下直接使用UIStackView一样,而且支持Interface build。