5.4 布局容器:枝条的堆叠方式


5.4 布局容器:枝条的堆叠方式

本节摘要: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 把共同修饰符打包——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 布局口诀——"先撑开,再对齐"。任何元素位置不对,先问它的容器占多大,再问它自己怎么对齐。

本节要点回顾

  • 三容器两助手:V/H/ZStack 管排列,Spacer 撑缝、frame 定形,Group 只批发修饰符不改布局;
  • 协商三步:父给建议、子报理想、父定终局,压缩按 layoutPriority 排队;
  • 容器尺寸由内容决定,要通栏先 frame 撑满——"先撑开,再对齐";
  • ZStack 尺寸取最大孩子,其余按对齐落位;安全区用 ignoresSafeArea 处理;
  • 诊断三板斧:底色可视化、查 frame、查 alignment,SwiftUI 版的视图树听诊。

树冠的最后一块是列表与导航——下一节 List 与 NavigationStack。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U