UIStackView
在iOS9
中苹果在UIKit
框架中引入了一个新的视图类UIStackView
。UIStackView
类提供了一个高效的接口用于平铺一行或一列的视图组合。stackView
视图管理着所有在它的arrangedSubviews
属性中的视图的布局。这些视图根据它们在 arrangedSubviews
数组中的顺序沿着stackView
视图的轴向排列。并可以根据当前屏幕大小和方向的变化动态调整它的内容,感觉起来就像是一个隐形的容器。实际上subviews
的位置是根据设置的对齐、间距和大小属性来决定的。
内部的原理是UIStackView
类管理了Auto Layout
约束。想象一下stackView
其实就是一个基于 Auto Layout
的抽象层从而使布局属性的创建简单化。你可以在一个主stackView
中嵌套stackView
从而让视图精确放置到相应的位置。
注意:我们只负责定义
UIStackView
的位置(position),UIStackView的大小(size)是可选的。当没有设置size
的时候,UIStackView
会根据它的内容的大小来调整自己的大小,即子视图各个控件的大小决定了UIStackView
的大小,当子视图的各个控件大小为0,那么UIStackView的大小同样0。
UIStackView的相关属性
在Storyboard
中可以搜索UIStackView
进行使用,可以看到包含水平和垂直两个StackView
Axis
: 轴,定义stackView
的方向,包括水平方向(Horizontal
)【子视图水平排列】和垂直方向(vertical
)【子视图垂直排列】,简单一点就是x或者y轴方向的确定
Alignment
: Alignment
控制subView
对齐方式
垂直方向包括4种对齐方式
水平方向包括6种对齐方式
如果值是Fill
则会调整子视图以适应空间变化,其他的值不会改变视图的大小。
Fill:子视图填充StackView。
Leading:靠左对齐。
Trailing:靠右对齐。
Center:子视图以中线为基准对齐。
Top:靠顶部对齐。
Bottom:靠底部对齐。
First Baseline:按照第一个子视图中文字的第一行对齐。
Last Baseline:按照最后一个子视图中文字的最后一行对齐。
Distribution
:填充形式,或者理解为定义subview
的分布方式,简单一点理解就是宽或者高的排列情况
Fill属性:
沿Axis
方向填充所有区域,为了实现这个功能,UIStackView
会扩大其中的一个子视图填充额外的区域,该view
有着最低的水平内容紧靠优先级(lowest horizontal content hugging priority
),如果所有的view
的优先级相等,那么会扩大第一个view
。
如果设置了spacing
,那么这些 arrangedSubviews
之间的间距就是spacing
。如果减去所有的spacing
,所有的arrangedSubview
的固有尺寸(intrinsicContentSize
)不能填满或者超出stackView
的尺寸,那还是会按照Hugging
或者 CompressionResistance
的优先级来拉伸或压缩一些 arrangedSubview
。如果出现优先级相同的情况,就按排列顺序来拉伸或压缩。
Equal Spacing属性:
等间距排列,这种是使arrangedSubview
之间的spacing
相等,但是这个spacing
是有可能大于stackView
所设置的spacing
,但是绝对不会小于。这个类型的布局可以这样理解,先按所有的 arrangedSubview
的 intrinsicContentSize
布局,然后余下的空间均分为spacing
,如果大于stackView
设置的spacing
那这样就OK了,如果小于就按照stackView
设置的spacing
,然后按照 CompressionResistance
的优先级来压缩一个 arrangedSubview
。
Fill Equally属性:
这种就是stackView
的尺寸减去所有的spacing
之后均分给 arrangedSubviews
,每个 arrangedSubview
的尺寸是相同的。
Equal Proportionally属性:
这种跟FillEqually
差不多,只不过这个不是将尺寸均分给arrangedSubviews
,而是根据 arrangedSubviews
的intrinsicContentSize
按比例进行分配。
Equal Centering属性:
在布局方向上居中的 subviews
等间距排列。这种是使 arrangedSubview
的中心点之间的距离相等,这样每两个 arrangedSubview
之间的spacing
就有可能不是相等的,但是这个spacing仍然是大于等于 StackView
设置的spacing
的,不会是小于。这个类型布局仍然是如果 StackView
有多余的空间会均分给 arrangedSubviews
之间的spacing
,如果空间不够那就按照 CompressionResistance
的优先级压缩arrangedSubview
。
Spacing 间距
子控件之间的最小间距,之所以说是最小间距,因为stackView会根据一定的规则对内部空间布局,有的时候不能满足所有要求,比如:stackView
本身宽度100,内部两个控件,宽度都为50,50+50+10就超过了本身宽度,这时会压缩其中一个子控件的宽度来满足最小间距。
其他重要属性
- subView和arrangedSubView
开始使用Stack View
前,我们先看一下它的属性subViews
和arrangedSubvies
属性的不同。如果想为stackView
添加子视图,应该调用addArrangedSubview:
或insertArrangedSubview:atIndex:
,arrangedSubviews
数组是subviews
属性的子集等方法。
要移除stackView
中的subview
,需要调用removeArrangedSubview:
和removeFromSuperview
。移除arrangedSubview
只是确保Stack View
不再管理其约束,而非从视图层次结构中删除,理解这一点非常重要,而调用removeFromSuperview
才是真正的去除。
- UIStackView的Position和size
虽然stackView
允许我们不直接使用Auto Layout
布局它的内容,但是我们仍然需要使用Auto Layout
来布局它的位置。基本上至少需要两个,即x,y的布局
来定义stackView
显示的位置。如果没有额外的约束,那么系统会自动根据stackView
的内容计算它显示的大小。
1、根据stackView
布局的方向决定计算大小的方式,简而言之,它的大小等于所有子控件在布局方向上的大小之和后加上子控件之间的间距。
对于水平方向的stackView
,各个子控件宽度相加+间距为stackView
的总宽度,高度为子控件中高度最大的视图。
对于垂直方向的stackView
,它的宽度为子视图最大视图的宽度,高度为各个子视图的高度之和+子视图之间的间距。。
我们也可以提供额外的约束来指定stackView
的高、宽或者都指定。在指定之后,stackView
会调节自己的布局和大小,让被管理的子视图填充对应的区域。准确的布局还得看stackView
布局属性的设置。如:distribution,alignment
等。
- 动态的改变UIStackView的内容
stackView
会根据视图被添加、去除、插入到arrangedSubviews
来动态更新布局,或者设置被管理视图的isHidden
属性来控制布局,也可以更新布局方向(axis),这一点非常有用,能够很好的动态适配内容进行显示。
下面看一个小功能,效果如下
主要是简单的使用了一下UIStackView
的嵌套,最底层是一个垂直stackView
,然后为其添加相应的约束,并依次创建子视图添加到stackView
,子视图包括两个label
,一个水平的stackView
,一个按钮。这里水平方向的stackView
,该视图添加了3个按钮等间距排列,最好的Press Me按钮实现了点击隐藏和显示水平的stackView
import UIKit
import SnapKit
class ViewController: UIViewController {
var stackView: UIStackView = UIStackView()
var nestedStackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical //垂直方向布局
stackView.alignment = .fill
stackView.spacing = 20
stackView.distribution = .fillEqually
stackView.backgroundColor = UIColor.brown //注意:这里设置背景颜色无效,说明stackView并没有被渲染到屏幕上
view.addSubview(stackView)
stackView.snp.makeConstraints { (make) in
make.left.right.top.bottom.equalToSuperview().inset(20)
}
var label = UILabel()
label.text = "Label 1"
label.backgroundColor = UIColor.red
stackView.addArrangedSubview(label)
label = UILabel()
label.text = "Label 2"
label.backgroundColor = UIColor.cyan
stackView.addArrangedSubview(label)
nestedStackView.axis = .horizontal // 水平方向布局
nestedStackView.alignment = .fill
nestedStackView.spacing = 20
nestedStackView.distribution = .fillEqually
nestedStackView.addArrangedSubview(UIButton(type: .infoDark))
nestedStackView.addArrangedSubview(UIButton(type: .infoLight))
nestedStackView.addArrangedSubview(UIButton(type: .contactAdd))
stackView.addArrangedSubview(nestedStackView)
// 添加按钮实现隐藏nestedStackView功能
let button = UIButton(type: .custom)
button.setTitle("Press Me", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.addTarget(self, action: #selector(pressMe(button:)), for: .touchUpInside)
stackView.addArrangedSubview(button)
}
@objc func pressMe(button: UIButton) {
UIView.animate(withDuration: 0.5) {
self.nestedStackView.isHidden = !self.nestedStackView.isHidden
}
}
}
可以看到代码非常简单,并不需要像以前一样设置每一个视图的约束,使用stackView
,只需要为最外层的stackView
添加约束即可。
实际应用场景
首先看一下需要实现的效果图
由图可知实现并不复杂,都是一些常见的UI控件,但是为了更容易布局,其实我们可以使用UIStackView
,比如:上半部分的用户头像、用户名称、用户描述我们就可以使用一个UIStackView
,减少不必要的布局约束,而且显示用户名称和大v也可以使用UIStackView,实现起来更加简单方便。这里简单看一下用户名称和大v图片的显示实现。
图看起来很简单,不就是一个label
显示用户名称,一个imageView
显示用户的icon嘛?或者有些人觉得一个button
也可以搞定,但是我们先了解一下需求:
1)需要考虑是否是大v,然后确定是否显示icon
2)名字的文字内容多少是不固定的,有的长又的短,超过限制宽度需要使用...代替
3)如果是大v,那么名字和icon整体的显示要居中显示,如果不是大v,那么名字单独居中,icon不显示
分析之后,想想只使用一个button
或者使用label
和imageView
可以实现,但是有点小麻烦,一起来看一下:
实现效果
一个tableView
简单的展示相关的一些显示情况,具体看一下是如何布局的:
1、添加子视图,包括两个部分,头像+容器视图
func setupSubViews() {
containView.addSubview(avatar)
contentView.addSubview(containView)
// 头像约束
avatar.translatesAutoresizingMaskIntoConstraints = false
avatar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
avatar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15).isActive = true
// 容器视图添加约束
containView.translatesAutoresizingMaskIntoConstraints = false
containView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 20).isActive = true
containView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: 20).isActive = true
containView.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 10).isActive = true
containView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
containView.backgroundColor = UIColor.cyan
}
2、布局容器视图
func setupContainView() {
// 添加子视图
containView.addSubview(nameLabel)
containView.addSubview(img)
// 给label添加约束
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.leadingAnchor.constraint(greaterThanOrEqualTo: containView.leadingAnchor).isActive = true
nameLabel.topAnchor.constraint(equalTo: containView.topAnchor).isActive = true
nameLabel.bottomAnchor.constraint(equalTo: containView.bottomAnchor).isActive = true
nameTrailingConstraint = nameLabel.trailingAnchor.constraint(equalTo: img.leadingAnchor) // 引用约束
nameTrailingConstraint.isActive = true
// 给imageView添加约束
img.translatesAutoresizingMaskIntoConstraints = false
img.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor).isActive = true
img.trailingAnchor.constraint(lessThanOrEqualTo: containView.trailingAnchor).isActive = true
// 设置label的内容压缩较低,当内容过多时,压缩label的显示内容
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
}
3、添加nameLabel
的布局以实现居中显示
func showVIPIcon(_ show: Bool) {
// 失效原有约束
NSLayoutConstraint.deactivate([nameTrailingConstraint])
if show {
nameTrailingConstraint = nameLabel.trailingAnchor.constraint(equalTo: img.leadingAnchor, constant: 0)
} else {
// 17是图片的大小,相当于盖住图片
nameTrailingConstraint = nameLabel.trailingAnchor.constraint(equalTo: img.leadingAnchor, constant: 17)
}
// 生效新的约束
NSLayoutConstraint.activate([nameTrailingConstraint])
img.isHidden = !show
}
4、在代理方法中赋值显示内容
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
cell.nameLabel.text = names[indexPath.row]
cell.showVIPIcon(vips[indexPath.row])
return cell
}
当然也可以不使用容器视图,单独对nameLabel
进行布局调整,有兴趣的可以尝试。但是我们在看一下使用UIStackView
1、将avatar
和stackView
添加到contentView
上,然后只需要对avatar
和stackView
布局设置属性即可
func setupStackView() {
contentView.addSubview(avatar)
contentView.addSubview(stackView)
// 头像约束
avatar.translatesAutoresizingMaskIntoConstraints = false
avatar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
avatar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15).isActive = true
// 添加约束
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 20).isActive = true
stackView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: 20).isActive = true
stackView.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 10).isActive = true
stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
stackView.axis = .horizontal // 水平布局
stackView.alignment = .center
stackView.spacing = 0 // 设置子控件之间的距离
stackView.distribution = .equalCentering
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(img)
}
2、是否显示大v,非常简单,控制图片的显示就好,因为stackView
会自动根据是否显示图片来调整布局
func showVIPIcon(_ show: Bool) {
img.isHidden = !show
}
效果:
UIStackView与枚举
我们使用UIStackView
的时候需要创建基本的UI控件,为了让创建UI控件更加简单,所以配合枚举进行使用,详情内容看这里,下面一起看一下代码实现
1、定义枚举
enum ContentElement {
case label(String)
case button(String, UIColor, () -> ())
case image(UIImage)
}
2、扩展枚举,声明计算属性用于添加子视图
// 定义枚举来创建UI
extension ContentElement {
var view: UIView {
switch self {
case .label(let text):
let label = UILabel()
label.numberOfLines = 0
label.text = text
return label
case .button(let title, let titleColor, let callback):
return CallbackButton(title: title, titleColor: titleColor, onTap: callback)
case .image(let image):
let imgView = UIImageView(image: image)
imgView.contentMode = .scaleAspectFit
return imgView
}
}
}
3、处理button
的响应事件
final class CallbackButton: UIView {
let onTap: () -> Void
let button: UIButton
init(title: String, titleColor: UIColor, onTap: @escaping () -> Void) {
self.onTap = onTap
self.button = UIButton(type: .custom)
super.init(frame: .zero)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle(title, for: .normal)
button.setTitleColor(titleColor, for: .normal)
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
button.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
button.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
button.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
button.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func tapped(sender: AnyObject) {
onTap()
}
}
4、扩展UIStackView
,提供遍历的便捷化方法
extension UIStackView {
convenience init(elements: [ContentElement]) {
self.init()
translatesAutoresizingMaskIntoConstraints = false
for element in elements {
addArrangedSubview(element.view)
}
}
}
5、简单使用一下
class UIStackViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let elements: [ContentElement] = [
.image(UIImage(named: "avatar")!),
.label("To use the Swift Talk app please login as a subscriber"),
.button("Login with GitHub", UIColor.red,{
print("Button tapped")
}),
.label("If you're not registered yet, please visit http://objc.io for more information")
]
let stack = UIStackView(elements: elements)
stack.axis = .vertical
stack.spacing = 10
view.addSubview(stack)
stack.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
}
效果如下