UIStackView基础知识

本文参考Apple官方文档

UIStackView是在iOS9的时候发布的一个新特性,用来将一些视图按照纵向或者横向,以流线形式布局。可以帮助开发者更好更方便的布局,很多场景都用得到。

UIStackView简介

stack view可以让用户借助Auto Layout的帮助,来创建出可以动态适应设备方向,屏幕不同尺寸和空间变化的视图布局。stack view有一个属性arrangedSubviews,包含着需要被stack view管理布局的所有视图,这个属性里包含的所有views都会根据在这个数组的顺序,依次沿着stack view的轴线分布。stack view精确的布局主要还是取决于它的几个关键布局属性,axis,distribution,alignmentspacing还有其他的一些属性。这几个属性什么概念呢,可以看一下下图,有个大概的了解,下图是一个轴线为水平方向的stack view:

水平方向的stack view

其实,Alignment就是垂直于轴线方向的对齐方式;

Distribution就是轴线方向的分布方式;

Spacing就是视图之间的间距;

在storyboard里使用stack view是非常简单的。我们只从组件库内拖拽一个UIStackView到storyboard里,然后固定好位置,然后往stack view里拖入我们想要的视图组件即可。stack view会基于内部的view,以及我们设置的布局属性来调整对应的布局。

我们需要指定stack view的起点(position),但是stack view的尺寸,也就是长宽是不一定要固定的,stack可以根据内容自动调整它的尺寸,当然我们也可以直接固定它的长宽。

UIStackView和Auto Layout的关系

在使用stack view的时候,虽然我们可以不用手动去添加auto layout,但是实际上,stack view本身已经使用了Auto Layout来布局被它管理的视图。譬如说,在水平方向的stack view里,它所管理的第一个视图的左边会紧靠着stack view的左边,stack view里的最后一个视图的右边会紧靠着stack view的右边。相对应的,在垂直方向的stack view里,第一个和最后一个视图会相对应的紧靠着stack view的上边和下边。如果我们设置了isLayoutMarginsRelativeArrangement这个属性为true,那么前面的例子第一个视图和最后一个视图会紧靠的是相对应的margin,而不是edge了。其实stack view能够做出上述的布局,本质上就是利用了Auto Layout,只是它帮我们默认添加好了而已。

同理,当我门设置stack view的一些布局属性的时候,其实底层也是再操作一些Auto Layout,只是无需我们自己添加,而是现在变得更加简单,我们只需要更改stack view对应的布局属性即可。

对于所有沿着轴线方向的布局来说,除了UIStackView.Distribution.fillEqually这种布局之外,stack view都会用每一个被管理的view的intrinsicContentSize属性来计算轴线方向的尺寸。但是UIStackView.Distribution.fillEqually这种布局的时候,stack view会重新计算轴线方向的尺寸,好让每一个被管理的view尺寸相同,stack view会尽可能的拉升所有被管理的views,好让他们在沿着轴线方向上的尺寸和拥有最大intrinsic size的view相等。

对于所有垂直于轴线方向的对齐方式来说,除了UIStackView.Alignment.fill这种对齐方式之外,stack view都会用每一个被管理的view的intrinsicContentSize属性来计算垂直于轴线方向的尺寸。但是UIStackView.Alignment.fill这种对齐方式,stack view会重新计算垂直于轴线方向的尺寸,好让每一个被管理的view填满stack view,stack view会尽可能的拉升所有被管理的views,好让他们在垂直于轴线方向上的尺寸和拥有最大intrinsic size的view相等。

定位和调整stack view的大小

虽然stack view可以不直接使用auto layout来布局它里面的内容,但是stack view本身还是需要auto layout来布局的。通常情况下,需要至少要钉住两条相近的边才能确定stack view的起点。stack view的具体尺寸,也就是它的长宽可以通过里面的内容,四通自动计算出来。

  • 在stack view的轴线方向,stack view的尺寸 = 所有被管理的view轴线方向的尺寸之和 + 轴线方向的所有间距之和

  • 在stack view垂直于轴线的防线,stack view的尺寸 = 所有被管理的view当中垂直于轴线方向的最大尺寸

  • 如果设置了isLayoutMarginsRelativeArrangement这个属性为true,那么stack view的尺寸就要算上margin边距

我们也可以对stack view再添加一些额外的约束,譬如指定宽,高等等,stack view都会根据这些来调整布局和尺寸之类。

stack view的一些通用用法

只确定stack view的位置

我们可以通过锁定stack view两条相邻的边到其父视图的距离,来确定stack view的位置。在这种情况下,stack view的尺寸会随着被管理的视图,在两个方向上伸缩。当你想要stack view的内容都接近它的intrinsic content size,并且你想要根据这个stack view来管理其他界面元素的布局,那么这个方法很有用。

只固定stack view的位置

在stack view轴线方向确定尺寸

在这种情况下,除了确定stack view的位置以外,还会对stack view添加一个约束,用以确定stack view在轴线方向上的尺寸。那么在轴线方向上,stack view会根据你设置的布局属性,来填满stack view;在垂直于轴线的方向上,stack view会自由伸缩,垂直于轴线方向的尺寸=所有被管理的视图中,垂直于轴线方向上,尺寸最大的那一个。

在stack view轴线方向确定尺寸

在垂直于stack view轴线方向确定尺寸

这种情况和上面的差不多,但是它是固定了位置和垂直于轴线方向的尺寸。那么stack view在轴线方向上的尺寸,会随着你添加或者删除,自动伸缩。除非你使用fillEqually,不然的话被管理的视图都会根据它实际的尺寸进行布局。垂直于轴线方向上的对齐方式取决于alignment属性。

在垂直于stack view轴线方向确定尺寸

确定stack view的位置和尺寸

在这种情况下,我们已经为stack view添加了约束,固定了它的位置和尺寸,那么stack view只能在固定大小的空间里,根据设置好的布局属性来布局。

确定stack view的位置和尺寸

在stackview中使用自定义视图

前面我们讲了很多,但是有个很重要的问题,就是当我们用stackview来布局我们自定义的view,或者一个普通的uiview的时候,你会发现,根本就没用,运行的时候就是一片空白,无法展示我们自定义的view,那是因为,stackview在布局其中的视图的时候,默认使用的是每个视图的IntrinsicContentSize属性。然后我们自定义的view以及普通的UIView是没有这个属性的,看下表:

ViewIntrinsic content size
UIView and NSViewNo intrinsic content size.
SlidersDefines only the width (iOS).Defines the width, the height, or both—depending on the slider’s type (OS X).
Labels, buttons, switches, and text fieldsDefines both the height and the width.
Text views and image viewsIntrinsic content size can vary.

所以,当你往stackview里添加一个自定义视图的时候,其实stackview是无法确认它的尺寸并布局的,所以如果我们希望stackview对我们的自定义视图起作用,那么我们就需要在自定义视图中重载IntrinsicContentSize属性,如下:

1
2
3
override var intrinsicContentSize: CGSize {
return CGSize(width: 100, height: 100)
}

那么,stackview就可以管理我们自定义视图的布局了。

管理UIStackView的布局和展示

其实UIStackView是一个没有被渲染的UIView的子类。它不是可视化的,肉眼看不见,不提供视觉界面,但是它可以管理在它里面视图的布局,位置,尺寸等。因此,一些UIView的属性,譬如backgroundColor,在stack view上是不起作用的。同样的,我们也无法重载stack view的layerClass,draw(_:),draw(__:in)这些方法。

以下属性会影响stack view如何布局它里面的内容视图:

  • axis:决定了stack view的方向,要么是垂直方向,要么是水平方向

  • distribution:决定了沿着stack view的轴线方向的分布

  • alignment:决定了垂直于轴线方向的布局

  • spacing:决定了被管理视图之间的最小间距

  • isBacelineRelativeArrangement:决定视图之间的垂直间距是否从底线开始算起

  • isLayoutMarginsRelativeArrangement:决定了stack view是否根据margins来布局其中的内容

通常来说,我们用一个stack view来布局少量的视图元素。但是我们可以通过stack view内嵌一个stack view来创建更多复杂的视图层级。

同样的,我们也可以为stack view内部被管理的view再添加一些约束,用来做一些额外的布局,但要避免产生冲突。

stack view的subviews和arrangedSubviews的关系

stack view有一个arrangedSubviews属性,里面是全部被stack view管理布局的视图,它是subviews的子集。严格来讲,stack view会保证以下几条规则:

  • 当stack view添加一个view到它的arrangedSubviews,如果这个view之前并不存在,那它必定也会被添加到subviews

  • 当一个subview从stack view里移除了,那么stack view也一定会把这个view从arrangedSubviews里移除。

  • 把一个view从stack view的arrangedSubviews这个数组里移除,并不会把它从subviews里移除。只是stack view不再管理这个view的位置和布局,但是这个view依然在stack view里,作为一个子视图,如果可见的话,也会被渲染。

虽然arrangedSubviews属性包含的视图是subviews的子集,但是这些数组里顺序确实相互独立的。

  • arrangedSubviews里的顺序决定了stack view里视图显示的顺序。随着index从小到大,垂直方向是从上到下,水平方向是从左到右。

  • subviews里的顺序决定了视图层次在z轴上的顺序。index越大的view就约在上面。

动态的改变stack view的内容

当对stack view的arrangedSubviews这个数组添加,移除或插入view的时候,或者其中的一个viewisHidden属性改变的时候,stack view都会自动更新它的布局。

1
2
3
4
// Appears to remove the first arranged view from the stack.
// The view is still inside the stack, it's just no longer visible, and no longer contributes to the layout.
let firstView = stackView.arrangedSubviews[0]
firstView.isHidden = true

我们也可以将这个属性的变化用动画演示,如下:

1
2
3
4
5
// Animates removing the first item in the stack.
UIView.animateWithDuration(0.25) { () -> Void in
let firstView = stackView.arrangedSubviews[0]
firstView.isHidden = true
}

我们也可以为不同的size class指定不同的布局,那么随着不同设备的size class的切换,系统会自动用动画演示这些转变。

UIStackView相关属性和API

管理布局视图

1
func addArrangedSubview(_ view: UIView)

将指定的view添加进到arrangedSubviews这个数组的末尾。如果这个view不存在stack view里,那么也会添加进subviews内,如果这个view已经存在stack view里了,那么就不会通知subviews

1
var arrangedSubviews: [UIView] {get}

一个UIView类型的数组,包含了所有被stack view 管理布局的view,是subviews的一个子集。

1
func insertArrangedSubview(_ view: UIView, at stackIndex:Int)

将指定的view添加进到arrangedSubviews这个数组指定的index。如果这个view不存在stack view里,那么也会添加进subviews内,如果这个view已经存在stack view里了,那么就不会通知subviews

1
func removeArrangedSubview(_ view: UIView)

arrangedSubviews这个数组里移除指定的view,但是并没有从subviews里移除这个view。也就是说,stack view只是不再管理这个view的位置,布局等,但是依然存在在subview的视图层次内。

如果我们希望从arrangedSubviews里删除这个view后,不再看到这个view,那么我们要执行removeFromSuperview()这个方法,或者我们需要把这个view的isHidden属性设为true。

设置布局属性

1
var alignment: UIStackView.Alignment {get set}

决定了垂直于轴线方向的布局。默认值是UIStackView.Alignment.fill。这是个枚举值,如下:

  • fill:让视图在垂直于轴线的方向尽可能填满可以被填充的空间

  • leading:只有stack view在垂直方向时有用,就是左对齐

  • top:只有stack view在水平方向时有用,就是上对齐

  • firstBaseLine:stack view根据第一个基线来对齐它的视图,只在stack view水平方向时有用

  • center:居中对齐

  • trailing:只有stack view在垂直方向时有用,就是右对齐

  • bottom:只有stack view在水平方向时有用,就是下对齐

  • lastBaseLine:stack view根据第最后一个基线来对齐它的视图,只在stack view水平方向时有用

1
var axis: NSLayoutConstraint.Axis{get set}

这个属性决定了stack view内部被管理视图的排列方向。默认值是NSLayoutConstraint.Axis.horizontal,也就是水平方向。这是个枚举值,如下:

  • horizontal:表示水平方向

  • vertical:表示垂直方向

1
var isBaselineRelativeArrangement: Bool { get set }

布尔值属性,用来决定被管理视图之间的垂直间距是否要根据基线来测算。默认值是false。

1
var distribution: UIStackView.Distribution { get set }

决定了沿着stack view的轴线方向的视图分布。默认值是UIStackView.Distribution.fill,这是个枚举值,如下:

  • fill:stack view重新计算被管理视图的尺寸,让视图在轴线方向尽可能填满可以被填充的空间。当被管理的视图无法正好适配stack view的时候,它根据视图的抗压缩优先级收缩视图,当被管理的视图无法填满stack view的时候,它会根据hugging优先级来缩放视图。

  • fillEqually:stack view重新计算被管理视图的尺寸,让视图在轴线方向尽可能填满可以被填充的空间。被管理的视图在沿轴线方向的尺寸相同。

  • fillProportionally:stack view重新计算被管理视图的尺寸,让视图在轴线方向尽可能填满可以被填充的空间。被管理的视图根据它们沿轴线方向的固有尺寸,按照比例合理的安排它们的尺寸

  • equalSpacing:stack view指定被管理视图的位置,让视图在轴线方向尽可能填满可以被填充的空间。当被管理的视图无法填满stack view的时候,它会在视图之间均匀的填充间距;当被管理的视图无法正好适配stack view的时候,它根据视图的抗压缩优先级收缩视图

  • equalCentering:stack view会尝试指定被管理视图的位置,好让视图在轴线方向,每个视图中心点到中心点的距离相等,并且如果你设置了spacing属性,这个值也会被包含在内。当被管理的视图无法正好适配stack view的时候,它会拉伸视图之间的间距,直到这个间距达到spacing属性设置的最小值。如果这个时候被管理的视图还是无法正好适配stack view的时候,它根据视图的抗压缩优先级收缩视图。

1
var isLayoutMarginsRelativeArrangement: Bool { get set }

布尔值属性,用来决定stack view是否根据layout margins来布局被管理的视图。默认值是false

1
var spacing: CGFloat { get set }

stack view里两个相邻的被管理视图之间的最小间距。默认值是0.0

设置被管理视图之间的间距

1
func customSpacing(after arrangedSubview: UIView) -> CGFloat

返回指定视图后面的自定义间距

1
2
func setCustomSpacing(_ spacing: CGFloat, 
after arrangedSubview: UIView)

设置指定视图后面的自定义间距

1
class let spacingUseDefault: CGFloat

stack view里视图之间默认的间距

1
class let spacingUseSystem: CGFloat

系统定义的相邻视图之间的间距

Author: Uncle Peter
Link: http://yoursite.com/2017/08/09/UIStackView基础知识/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.