使用UIStackView进行灵活布局

一.引言

在 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 远不止本文中提到的这些用法。

无论是嵌套使用、动态插入视图,还是与动画、优先级配合,

它都还有不少值得深入挖掘的"妙用"。

后续如果有合适的场景,我们也可以再一起继续讨论。

相关推荐
linweidong9 天前
iOS 开发面试 50 个高频易混淆知识点详解
ios·设计模式·面试·cocoa·uikit·uiview·uistackview
Evand J9 天前
【代码介绍】自适应R的AEKF(自适应扩展卡尔曼滤波)和经典EKF比较,MATLAB例程|三维非线性系统
开发语言·matlab·ekf·自适应·自适应滤波
Evand J15 天前
【图像去噪例程】自适应窗口长度的滑动窗口中值滤波(附MATLAB下载链接)
图像处理·计算机视觉·matlab·滤波·自适应
胖虎12 个月前
我用一个 UITableView,干掉了 80% 复杂页面
ios·架构·cocoa·uitableview·ui布局
__Yvan3 个月前
解决ConstraintLayout中LinearLayout显示异常问题
android·xml·约束布局
dundunmm6 个月前
【论文阅读】Spatial-Temporal Graph Learning with Adversarial Contrastive Adaptation
论文阅读·自适应·对比·对抗·时空数据·时空图学习
CPU不够了9 个月前
WPF常见问题清单
wpf·自适应
日更嵌入式的打工仔9 个月前
PHY的自适应协商简析
网络·嵌入式硬件·自适应·phy
胖虎11 年前
Android 布局系列(四):ConstraintLayout 使用指南
android·xml·布局·constraint·约束布局