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

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

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

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

相关推荐
dundunmm4 天前
【论文阅读】Spatial-Temporal Graph Learning with Adversarial Contrastive Adaptation
论文阅读·自适应·对比·对抗·时空数据·时空图学习
CPU不够了4 个月前
WPF常见问题清单
wpf·自适应
日更嵌入式的打工仔4 个月前
PHY的自适应协商简析
网络·嵌入式硬件·自适应·phy
胖虎110 个月前
Android 布局系列(四):ConstraintLayout 使用指南
android·xml·布局·constraint·约束布局
假装我不帅10 个月前
tailwindcss学习03
tailwindcss·自适应
zhanggongzichu1 年前
跨端兼容——请让我的页面展现在电脑、平板、手机上
前端·css·vue·自适应·响应式·跨端兼容
柯南二号1 年前
Android 约束布局ConstraintLayout整体链式打包居中显示
android·约束布局
丁劲犇1 年前
基于准静态自适应环型缓存器(QSARC)的taskBus万兆吞吐实现
自适应·管道·静态·环状缓存·taskbus·吞吐