本节摘要:SwiftUI 没有约束方程,布局靠三种堆叠容器加两位调度助手:VStack 纵排、HStack 横排、ZStack 叠放,Spacer 撑缝、frame 定形。更深一层是父子协商机制——父提建议尺寸、子报理想尺寸、父最终落位,三步走完,理解它比背参数重要,因为一切"为什么不居中/为什么被压缩"的答案都在协商里。本节用详情页与添加表单两个实战把堆叠语法与协商规则一次讲透。
三种容器一句话分清:VStack 竖着摞、HStack 横着排、ZStack 叠着放。参数都只有两个——间距与对齐:
// VStack:竖排,spacing 管行距,alignment 管水平对齐 VStack(alignment: .leading, spacing: 12) { Text("龟背竹").font(.title) Text("观叶 · 喜湿").foregroundColor(.secondary) } // alignment 选项:.leading 左对齐 / .center 居中 / .trailing 右对齐 // 不写时默认 .center 与系统默认间距 // HStack:横排,同样的两个参数 HStack(spacing: 8) { Circle().fill(.green).frame(width: 10, height: 10) Text("状态良好") Spacer() // 调度助手一:弹性空隙,把后面的推到最右 Image(systemName: "chevron.right").foregroundColor(.secondary) } // ZStack:同框叠放,后写的在上层 ZStack(alignment: .bottomTrailing) { Image("monstera").frame(width: 160, height: 120) // 底层照片 Text("已收藏") // 右下角浮层 .font(.caption) .padding(6) .background(.black.opacity(0.6), in: Capsule()) // 胶囊底 .foregroundColor(.white) } // 4.5 进度环的 SwiftUI 版就是 ZStack:圆环底 + 弧 + 中心文字
两位调度助手是排版的关节。Spacer 是"会吃掉所有剩余空间的空隙":一个 Spacer 制造两端布局,两个夹着中间元素制造居中,flex 布局的老概念在这里复活。frame 是"给自己定形":宽、高、对齐三组参数,还能只定一边(.frame(maxWidth: .infinity) 撑满可用宽,等于 UIKit 的左右贴边)。
// frame 的三种经典用法 Text("确认存入花园") .frame(maxWidth: .infinity) // 撑满宽度(文字本身不变,容器变宽) .background(.mint) // 绿条通栏——比 padding 版背景大一圈(5.2 洋葱) Text("还剩 2 天") .frame(width: 88, height: 88) // 定形成方框 .background(.gray.opacity(0.15)) // 做成"天数徽章" Image(systemName: "drop.fill") .frame(minWidth: 0, maxWidth: 44) // 弹性区间:最小零最大四十四
参数背完只是入门,SwiftUI 布局的内核是一场三步协商,一切疑难杂症的病灶都在这里:
// 协商三例,全是高频疑问 // 例一:文字为什么被截断?——父给了小空间,文字被压缩 HStack { Text("龟背竹 Monstera deliciosa 天南星科") Spacer() Text("还剩 2 天") } // 窄屏时左侧长文字被截断:两段文字默认同级,谁长谁吃亏 // 解法:给短的那段提高保持优先级,长文字让路 HStack { Text("龟背竹 Monstera deliciosa 天南星科") .lineLimit(1) // 允许单行省略 .truncationMode(.middle) // 省略号打中间,头尾都保留 Spacer() Text("还剩 2 天") .layoutPriority(1) // 优先级一:这行绝不压缩 } // 例二:为什么不居中?——Spacer 没吃满 VStack { Text("标题") HStack { // 这个 HStack 宽度=内容宽度,没占满 Text("左"); Spacer(); Text("右") } .frame(maxWidth: .infinity) // 想左右贴边:先让 HStack 撑满父亲 .background(.yellow.opacity(0.2)) // 加底色可视化:加这行前黄条只包住内容 } // 例三:ZStack 的尺寸由谁定?——由最大的孩子定,其他孩子在对齐处等待 ZStack(alignment: .topLeading) { Rectangle().fill(.mint).frame(width: 200, height: 100) // 尺寸担当 Text("左上角") // 跟着对齐到左上 } // ZStack 自己的理想尺寸 = 最孩子的最大尺寸,不是孩子的和
第 4 章的 UIKit 详情页搬到 SwiftUI,整页布局三种容器一次用齐:
struct PlantPageView: View { let plant: Plant var body: some View { VStack(spacing: 16) { // 主轴:纵向大分区 ZStack(alignment: .bottomLeading) { // 照片区:图加浮层角标 Rectangle().fill(.green.opacity(0.25)) .frame(height: 180).frame(maxWidth: .infinity) Text(plant.kind.rawValue) .font(.caption.bold()) .padding(8) .background(.ultraThinMaterial, in: Capsule()) .padding(10) } HStack(alignment: .top, spacing: 12) { // 信息区:名字与状态并排 VStack(alignment: .leading, spacing: 4) { Text(plant.name).font(.title2.bold()) Text("上次浇水:两天前").font(.caption).foregroundColor(.secondary) } Spacer() VStack { Text("\(plant.remainingDays)") .font(.system(size: 34, weight: .heavy)) .foregroundColor(plant.shouldWater ? .red : .green) Text("天后浇水").font(.caption2) } } .padding(.horizontal) Button("浇水") { } .buttonStyle(.borderedProminent) .frame(maxWidth: .infinity) // 按钮通栏 .padding(.horizontal) Spacer() // 把整页顶到上沿,底部留白 } .ignoresSafeArea(edges: .top) // 照片区顶到状态栏下 } }
三个细节值得标注:嵌套对齐逐层生效(外层 VStack 管大分区间距、内层 VStack 管文字行距);padding(.horizontal) 只加左右不加上下(比全包 padding 精确);ignoresSafeArea 是 SwiftUI 版的安全区处理(3.5 的 safeAreaLayoutGuide 在这里的对偶物)。
成排的表单控件用 Group 把共同修饰符打包——Group 不是布局容器(不改变排列),是"修饰符批发处":
struct SettingsForm: View { @State private var reminderOn = true @State private var interval = 3 var body: some View { Form { Section("提醒") { Toggle("接收浇水提醒", isOn: $reminderOn) if reminderOn { // 条件渲染:状态直接控制布局 Stepper("每 \(interval) 天", value: $interval, in: 2...15) } } Section("危险区") { Button("清空花园记录", role: .destructive) { } } } } } // 关掉开关 → Stepper 整行出现或消失——没有动画包装也自带过渡(可选 .animation 修饰符加强)
if 直接写进 body 是声明式的又一红利:3.4 那套"isEnabled 加 alpha 手写联动",在这里是一个条件分支的事。
背景:新人把按钮包进 HStack 想居中,按钮却贴左不动。操作:逐步加可视化排查。
// 事故版:按钮贴左 HStack { Button("浇水") { } }.background(.yellow.opacity(0.3)) // 黄色区域只有按钮那么宽——HStack 的尺寸由内容撑起,它自己也在父亲的 leading 处 // 诊断步一:让 HStack 撑满 HStack { Button("浇水") { } } .frame(maxWidth: .infinity) // 现在 HStack 通栏 .background(.yellow.opacity(0.3)) // 黄条通栏,按钮还在左边 // 诊断步二:加对齐 HStack { Button("浇水") { } } .frame(maxWidth: .infinity, alignment: .center) // 按钮居中,结案 // 另一条等价路:HStack 里左右各放一个 Spacer 把按钮夹中间
解读:诊断手段是"给可疑容器加半透明背景"——SwiftUI 没有视图树听诊器(3.2 那套 dump 用不上),底色可视化就是它的平替;病根都一样:容器尺寸由内容决定,没撑开就谈不上对齐。变式:同样的思路诊断 ZStack 浮层偏移、List 行内不贴边,三板斧都是"可视化、查 frame、查对齐参数"。
💡 关键直觉:SwiftUI 布局口诀——"先撑开,再对齐"。任何元素位置不对,先问它的容器占多大,再问它自己怎么对齐。
树冠的最后一块是列表与导航——下一节 List 与 NavigationStack。