
一.引言
在 UIKit 中,布局方式一向都比较直接,甚至可以说有些"生硬"。
即便使用 Auto Layout 来描述视图之间的约束关系,相比 SwiftUI 或前端常见的盒子布局模型,在灵活性上仍然略逊一筹。
在 UIKit 中,常见的处理方式通常只有两种:
- 为不同的数据状态创建多套 UI 样式。
- 或者在数据变化时,动态调整视图的约束。
这些方案本身并没有问题,但随着业务复杂度的提升,布局逻辑往往会分散在各处,维护成本也会随之上升。
相比之下,在 SwiftUI 这类响应式框架中,很多时候只需要控制视图的显示与隐藏,页面布局就可以自然地过渡到另一种状态。
那么,在 UIKit 中是否也能实现类似"随内容变化而自适应"的布局效果呢?
其实,自从 UIStackView 推出之后,UIKit 的布局灵活性已经有了不小的提升。
无论是等宽、等间距,还是空间的自动分配,UIStackView 都提供了现成的能力。
再配合一些更灵活的使用方式,往往可以避免频繁地修改约束。
这一篇文章,就来介绍一个非常实用的场景:
如何使用 UIStackView,让两个子视图自然地分布在父视图的两端。
二. 让两个子视图居于两侧
其实只是这个要求并不难,即使不使用UIStackView我们也可以轻松实现。
2.1 约束布局直接实现
我们只需要设置leftView与父视图的左侧约束,rightView与父视图的右侧约束,代码实现如下:
Swift
// 父视图
let backView = UIView()
backView.backgroundColor = .green
backView.layer.cornerRadius = 8.0
backView.layer.masksToBounds = true
self.view.addSubview(backView)
backView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(12.0)
make.height.equalTo(80.0)
make.centerY.equalToSuperview()
}
//1.左侧视图
let leftView = UIView()
leftView.backgroundColor = .red
leftView.layer.cornerRadius = 2.0
leftView.layer.masksToBounds = true
backView.addSubview(leftView)
leftView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.centerY.equalToSuperview()
make.height.equalTo(70.0)
make.width.equalTo(244.0)
}
//2.右侧视图
let rightView = UIView()
rightView.backgroundColor = .orange
rightView.layer.cornerRadius = 2.0
rightView.layer.masksToBounds = true
backView.addSubview(rightView)
rightView.snp.makeConstraints { make in
make.height.equalTo(70.0)
make.width.equalTo(88.0)
make.trailing.equalToSuperview()
make.centerY.equalToSuperview()
}
效果如下:
直接使用约束布局
2.2 使用UIStackView的 distribution 属性
在使用 UIStackView 进行布局时,distribution 是一个非常核心的属性。它决定了 arrangedSubviews 在主轴方向上如何分配可用空间。简单来说,distribution 解决的不是「视图放在哪」,而是 「多出来的空间应该给谁」。
UIStackView 提供了多种 distribution 策略,例如:
- .fill:优先按照子视图的约束和 intrinsicContentSize 进行布局,多余空间由内容优先级最低的视图进行拉伸或压缩。
- .fillEqually:所有 arrangedSubviews 在主轴方向上占用相同的空间大小。
- .fillProportionally:根据各子视图的 intrinsicContentSize 按比例分配空间。
- .equalSpacing:保证子视图之间的间距相等,多余空间会被分配到这些间距中。
- .equalCentering:保证子视图的中心点之间的间距相等。
回到本文最开始的需求:
- 两个子视图分布在父视图两端
- 中间区域自动撑开
- 子视图保持自身大小
从这些条件来看,.equalSpacing 是最贴近需求的选择。
Swift
stackView.distribution = .equalSpacing
完整实现代码如下:
Swift
// stackView
let stackView = UIStackView()
stackView.backgroundColor = .green
stackView.layer.cornerRadius = 8.0
stackView.layer.masksToBounds = true
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .equalSpacing
backView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.leading.trailing.equalToSuperview()
}
//1.左侧视图
let leftView = UIView()
leftView.backgroundColor = .red
leftView.layer.cornerRadius = 2.0
leftView.layer.masksToBounds = true
stackView.addArrangedSubview(leftView)
leftView.snp.makeConstraints { make in
make.height.equalTo(70.0)
make.width.equalTo(244.0)
}
//2.右侧视图
let rightView = UIView()
rightView.backgroundColor = .orange
rightView.layer.cornerRadius = 2.0
rightView.layer.masksToBounds = true
stackView.addArrangedSubview(rightView)
rightView.snp.makeConstraints { make in
make.height.equalTo(70.0)
make.width.equalTo(88.0)
}
效果如下:
直接UIStackView的distribution
2.3 使用UIStackView + 自定义弹簧视图
在上一小节中,我们通过 distribution = .equalSpacing,可以较为简单地实现两个子视图分布在父视图两端的效果。
不过当只剩下一个子视图时,布局行为就不再那么容易控制。
为了解决这个问题,我们可以引入一种更加通用、也更可控的方式:在 UIStackView 中间插入一个 spacer View。
Spacer View 的核心思路
Spacer View 本质上是一个 不承载任何内容的 UIView,它唯一的职责就是:吃掉多余的空间。
实现代码如下:
Swift
let stackView = UIStackView()
stackView.backgroundColor = .green
stackView.axis = .horizontal
stackView.alignment = .center
stackView.layer.cornerRadius = 8.0
stackView.layer.masksToBounds = true
backView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.height.equalTo(80)
make.center.equalToSuperview()
}
// 左侧视图
let leftView = UIView()
leftView.backgroundColor = .red
stackView.addArrangedSubview(leftView)
leftView.snp.makeConstraints { make in
make.width.equalTo(244)
make.height.equalTo(70)
}
// Spacer
let spacer = UIView()
spacer.snp.makeConstraints { make in
make.width.equalTo(self.view.frame.size.width - 244 - 88 - 24)
}
stackView.addArrangedSubview(spacer)
// 右侧视图
let rightView = UIView()
rightView.backgroundColor = .orange
stackView.addArrangedSubview(rightView)
rightView.snp.makeConstraints { make in
make.width.equalTo(88)
make.height.equalTo(70)
}
效果如下:
使用UIStackView+自定义弹簧视图
三. 变化-隐藏右侧视图
上面的三种实现方式都没问题,都可以完美还原UI设计的效果,但是假如设计要求当右侧数据没有时,整个右侧视图不显示且左侧视图居中显示。
那么阁下该如何应对呢?
3.1 直接使用约束布局
如果我们采用的是约束布局直接实现,当我们隐藏rightView时,显然leftView不会自动移动到中间,效果应该是如下:
直接约束布局隐藏右视图效果
如果想要让左侧视图居中,我们只好重新设置约束。
3.2 使用UIStackView的distribution属性
需要注意的是,当 UIStackView.distribution 设置为 .equalSpacing,并且 StackView 中只剩下一个 arrangedSubview 时,该子视图会被拉伸以填满整个 StackView。
这是因为 equalSpacing 依赖"多个子视图之间的间距"来分配空间,当子视图数量不足时,布局行为会退化为 .fill,从而导致子视图的宽度约束不再生效。
最终导致子视图被拉伸至与 StackView 等宽。
效果如下:
使用UIStackView的distribution隐藏右侧视图效果
倒也居中了,只是被拉伸了,可能有些场景还挺需要这个效果的,但是显然不是我们要的效果。
3.3 使用UIStackView+自定义的弹簧视图
这就厉害多了,不过如果我们只是隐藏右侧视图的话,弹簧视图仍然会把左侧视图固定到最左端,它就像SwiftUI中的Sapcer()。但是我们可以把rightView和弹簧一起隐藏。
效果如下:
使用UIStackView + 自定义弹簧视图隐藏右视图效果
应该算是完美实现了设计的要求。
四. 结语
UIStackView 并不是一个"万能布局工具",但它确实让 UIKit 的布局方式变得更加灵活、也更加接近"声明式"的思路。
在合适的场景下,通过合理地组合 axis、alignment、distribution,再配合 spacer 这类简单的结构设计,我们可以用更少的约束代码,完成原本需要频繁修改约束才能实现的布局效果。
更重要的是,UIStackView 提供了一种从"空间分配"角度思考布局问题的方式。
当我们理解了它的工作模型,也就更容易判断哪些需求适合交给 StackView,哪些场景仍然需要显式地调整结构或约束。
当然,UIStackView 远不止本文中提到的这些用法。
无论是嵌套使用、动态插入视图,还是与动画、优先级配合,
它都还有不少值得深入挖掘的"妙用"。
后续如果有合适的场景,我们也可以再一起继续讨论。