布局系统:VStack垂直布局、HStack水平布局、ZStack层叠布局、Spacer弹性空间、Padding与Frame

布局,是SwiftUI的骨架。说白了,你写的每个界面,最终都要靠这些容器把元素「摆」到正确的位置上。我个人习惯把布局系统比作搭积木——VStack、HStack、ZStack就是三种不同的积木底座,Spacer是弹簧,Padding是缓冲垫,Frame是尺寸模具。搞懂它们,你就能搭出任何想要的界面。

VStack:垂直堆叠,从上往下

VStack是最常用的布局容器。它把子视图从上到下依次排列。我在项目中遇到过不少新手,一上来就疯狂嵌套VStack,结果界面卡得不行。其实VStack本身很轻量,但要注意别滥用。

VStack {
    Text("第一行")
    Text("第二行")
    Text("第三行")
}

默认情况下,VStack会居中排列子视图。你可以通过alignment参数控制对齐方式,比如.leading左对齐、.trailing右对齐。间距用spacing控制,我一般习惯设成8或12,视觉上比较舒服。

小技巧: 如果你需要子视图之间间距不一样,别用spacing参数。直接在子视图之间插入Spacer或者单独设置padding,更灵活。

HStack:水平排列,从左到右

HStack和VStack是对称的。它把子视图从左到右水平排列。你想想看,一个典型的表单——左边是标签,右边是输入框——用HStack就能轻松搞定。

HStack {
    Text("用户名")
    TextField("请输入", text: $username)
}

嗯,这里要注意:HStack里的子视图如果太多,可能会超出屏幕宽度。我曾经在做一个多标签筛选栏时,一口气塞了8个按钮进去,结果右边几个直接看不见了。解决方案是用.fixedSize()或者配合ScrollView滚动。

避坑指南: 我曾经在HStack里同时用了多个Spacer,结果布局完全乱套。记住:Spacer会平分剩余空间,如果你放了两个Spacer,它们各占一半。想要精确控制,用Frame的minWidth/maxWidth更靠谱。

ZStack:层叠覆盖,前后关系

ZStack是三个容器里最特殊的。它不按顺序排列,而是把子视图一层层叠起来。就像Photoshop里的图层——后面的视图会盖住前面的。我经常用它来做背景叠加、遮罩效果、或者给卡片加阴影。

ZStack {
    Color.blue
    Text("Hello")
        .foregroundColor(.white)
}

上面这段代码,蓝色背景上显示白色文字。ZStack默认也是居中对齐,你可以通过alignment参数调整。比如.topLeading就把所有子视图对齐到左上角。

我个人习惯用ZStack来做「徽标」效果——比如在头像右上角显示未读消息数。用ZStack把头像和数字叠在一起,再配合.offset()微调位置,非常方便。

Spacer:弹性空间,撑开布局

Spacer是SwiftUI里最神奇的「隐形弹簧」。它不显示任何内容,只负责占据剩余空间。你把它放在哪里,哪里就会被撑开。

HStack {
    Text("左")
    Spacer()
    Text("右")
}

这段代码会把「左」推到最左边,「右」推到最右边。Spacer默认是均匀分配空间的,但你可以通过.frame(minWidth:)给它设置最小宽度。我在项目中遇到过一个问题:Spacer在ScrollView里会失效。为什么呢?因为ScrollView的内容宽度是无限的,Spacer永远占不满。解决办法是给ScrollView的内容设置一个明确的宽度。

核心理解: Spacer不是「推动」其他视图,而是「占据」剩余空间。其他视图被挤到边上,是因为Spacer把中间的空间全占了。

Padding:内边距,呼吸空间

Padding给视图周围添加空白区域。说白了,就是让元素别贴边,留点呼吸空间。SwiftUI提供了默认的padding值(通常是16点),但你可以自定义。

Text("Hello")
    .padding()          // 默认四周16点
    .padding(.leading)  // 只加左边
    .padding(20)        // 四周20点
    .padding([.top, .bottom], 10) // 只加上下

嗯,这里有个坑:padding的顺序会影响最终布局。如果你先设置padding再设置背景色,背景色会包含padding区域。反过来,先设置背景色再设置padding,背景色就不包含padding区域。我刚开始学SwiftUI时被这个坑过好几次。

个人经验: 我习惯把padding放在最后设置,这样视觉上更直观。先确定内容的大小和颜色,最后再调整间距。

Frame:尺寸模具,精确控制

Frame是SwiftUI里最强大的尺寸控制工具。它可以设置视图的宽度、高度,还能控制对齐方式和伸缩行为。我经常用Frame来做两件事:限制最大尺寸、设置固定比例。

Text("Hello")
    .frame(width: 100, height: 50)
    .frame(maxWidth: .infinity, alignment: .leading)
    .frame(minHeight: 44, idealHeight: 50, maxHeight: 100)

第一行:固定宽100、高50。第二行:宽度撑满父视图,左对齐。第三行:最小高度44,理想高度50,最大高度100。Frame的参数组合非常灵活,我建议你多试试。

我曾经在做一个自适应卡片布局时,用Frame的aspectRatio参数实现了图片的等比缩放。比如设置.aspectRatio(16/9, contentMode: .fit),图片就会自动保持16:9比例,不会变形。

避坑指南: 我曾经在VStack里给子视图同时设置了frame和padding,结果计算出来的总高度比预期多了两倍。原因是我在frame里设置了固定高度,又在外面加了padding,两者叠加了。记住:frame控制的是内容区域,padding是额外空间,它们会累加。

实战组合:一个典型的卡片布局

说了这么多,我们来个实战。下面这个卡片布局,用到了VStack、HStack、Spacer、Padding和Frame。你看看能不能一眼看出每部分的作用。

VStack(spacing: 12) {
    HStack {
        Text("标题")
            .font(.headline)
        Spacer()
        Text("更多")
            .font(.caption)
            .foregroundColor(.blue)
    }
    .padding(.horizontal)
    
    Text("这是一段描述文字,用来展示卡片的内容区域。")
        .font(.body)
        .padding(.horizontal)
    
    HStack {
        Text("2024-01-01")
            .font(.caption)
            .foregroundColor(.gray)
        Spacer()
        Text("阅读 123")
            .font(.caption)
            .foregroundColor(.gray)
    }
    .padding(.horizontal)
    .padding(.bottom, 8)
}
.frame(maxWidth: .infinity)
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 4)
.padding(.horizontal, 16)

这段代码生成一个标准的卡片:标题在左上角,「更多」在右上角,中间是描述文字,底部是日期和阅读数。整个卡片有圆角、阴影,左右留了16点边距。你想想看,如果没有Spacer,标题和「更多」就会挤在一起。没有Padding,文字就会贴边。没有Frame,卡片宽度就不会撑满。

总结一下: VStack/HStack/ZStack是骨架,Spacer是弹簧,Padding是缓冲,Frame是模具。把这四样东西用好,SwiftUI里90%的布局问题都能解决。剩下的10%,靠的是对alignmentGuideGeometryReader等高级工具的理解——那是后面章节的内容了。