3.1 布局的三种 Stack
Stack 是 SwiftUI 最基础的布局容器。三种 Stack 分别对应不同的排列方向,它们可以任意嵌套,构成复杂界面。
| 容器 | 排列方向 | 典型用途 |
|---|---|---|
HStack | 水平排列(左→右) | 工具栏、标题行、标签+值 |
VStack | 垂直排列(上→下) | 列表项、表单、页面主结构 |
ZStack | 深度叠加(后→前) | 遮罩、徽标、卡片覆盖层 |
HStack — 水平排列
HStack 将子视图从左到右水平排列。通过 spacing 参数控制子视图之间的间距,通过 alignment 参数控制垂直对齐方式。
// 基本用法:水平排列图标和文字
HStack {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text("收藏")
}
// spacing:设置子视图间距(默认约8pt)
HStack(spacing: 16) {
Text("左")
Text("中")
Text("右")
}
// spacing: 0 消除间距
HStack(spacing: 0) {
Rectangle().fill(.red).frame(height: 4)
Rectangle().fill(.green).frame(height: 4)
Rectangle().fill(.blue).frame(height: 4)
}
// alignment:垂直对齐方式
HStack(alignment: .top) { // .top / .center / .bottom / .firstTextBaseline
Text("小字").font(.caption)
Text("大字").font(.largeTitle)
}
VStack — 垂直排列
VStack 将子视图从上到下垂直排列。alignment 参数控制水平对齐方式。
// alignment:水平对齐方式
VStack(alignment: .leading, spacing: 8) {
Text("标题").font(.headline)
Text("这是副标题,比较长一些").font(.subheadline)
Text("正文内容").font(.body).foregroundColor(.secondary)
}
// alignment 可选值:
// .leading — 左对齐
// .center — 居中(默认)
// .trailing — 右对齐
ZStack — 深度叠加
ZStack 将子视图沿 Z 轴叠加,后写的视图显示在前面(更靠近用户)。常用于实现遮罩、角标等效果。
// 卡片上叠加角标
ZStack(alignment: .topTrailing) {
// 底层:卡片主体
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.1))
.frame(width: 120, height: 80)
// 上层:未读角标
Circle()
.fill(.red)
.frame(width: 20, height: 20)
.overlay {
Text("3").font(.caption2).foregroundColor(.white)
}
.offset(x: 8, y: -8)
}
// alignment 参数控制叠加基准点
// .topLeading / .top / .topTrailing
// .leading / .center / .trailing
// .bottomLeading / .bottom / .bottomTrailing
3.2 Spacer 与 Divider
Spacer — 弹性填充
Spacer 是一个透明的弹性视图,会尽可能地占据所有剩余空间。它是实现"把元素推到两端"等布局效果的核心工具。
// 把按钮推到两端
HStack {
Button("取消") { }
Spacer() // 撑开中间空间
Button("确定") { }
}
// 把内容推到顶部
VStack {
Text("我在顶部")
Spacer() // 推到底部的空间
}
.frame(maxHeight: .infinity)
// minLength:Spacer 的最小尺寸(防止被压缩为0)
Spacer(minLength: 20)
Divider — 分割线
Divider 绘制一条分割线。在 HStack 中是垂直线,在 VStack 中是水平线。
// 垂直分割线(在 HStack 中)
HStack {
Text("左")
Divider()
Text("右")
}
.frame(height: 40)
// 水平分割线(在 VStack 中)
VStack {
Text("第一节")
Divider()
Text("第二节")
}
3.3 padding 与 frame
这两个修饰符是布局的基础,理解它们的区别和配合方式非常重要。
padding 在视图内部添加空白区域(类似 CSS padding);frame 则给视图指定一个期望的外部尺寸,视图会在这个框架内对齐。
padding — 内边距
// 所有方向 16pt 内边距
Text("你好").padding(16)
// 系统默认内边距(约 16pt)
Text("你好").padding()
// 指定方向
Text("你好")
.padding(.horizontal, 20) // 水平方向 20pt
.padding(.vertical, 10) // 垂直方向 10pt
.padding(.top, 8) // 仅顶部
// 方向可选:.all / .horizontal / .vertical
// .top / .bottom / .leading / .trailing
frame — 尺寸框架
// 固定尺寸
Image(systemName: "photo")
.frame(width: 100, height: 100)
// 最小/最大尺寸:常用于响应式布局
Text("自适应宽度")
.frame(maxWidth: .infinity) // 尽可能宽
.frame(minHeight: 44) // 最小高度 44(标准触控区)
// alignment:内容在 frame 内的对齐方式
Text("左对齐")
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow.opacity(0.3))
// 常见模式:全屏宽度按钮
Button("登录") { }
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.foregroundColor(.white)
.cornerRadius(12)
SwiftUI 修饰符从内到外叠加。.padding().frame(width: 200) 表示先加内边距,再限制整体为 200pt 宽;.frame(width: 200).padding() 则先限定 200pt,外边的 padding 不影响内部宽度。
3.4 LazyVGrid 与 LazyHGrid
Lazy(懒加载)意味着只有当视图滚动进入屏幕可见区域时,对应的子视图才会被创建,适合展示大量数据。
GridItem — 列(行)定义
在使用网格布局前,需要先定义列(或行)的规格,使用 GridItem 数组描述。
| GridItem 尺寸类型 | 说明 | 示例 |
|---|---|---|
.fixed(n) | 固定宽/高度 | 每列固定 100pt |
.flexible(minimum:maximum:) | 弹性尺寸,自动填充 | 3列等宽自适应 |
.adaptive(minimum:maximum:) | 自适应列数,尽量多放 | 每列至少 80pt,放尽量多列 |
// 定义列规格
let fixedColumns = [
GridItem(.fixed(100)), // 第1列 100pt
GridItem(.fixed(100)), // 第2列 100pt
GridItem(.fixed(100)) // 第3列 100pt
]
// 3列等宽(弹性)
let flexibleColumns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
// 简写:重复 N 个相同规格
let threeColumns = Array(repeating: GridItem(.flexible()), count: 3)
// 自适应:每列至少 80pt,列数自动计算
let adaptiveColumns = [GridItem(.adaptive(minimum: 80))]
LazyVGrid — 垂直方向网格(向下扩展)
let items = Array(1...20) // 示例数据
ScrollView {
LazyVGrid(
columns: Array(repeating: GridItem(.flexible()), count: 3),
spacing: 12 // 行间距
) {
ForEach(items, id: \.self) { item in
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.3))
.aspectRatio(1, contentMode: .fit) // 保持正方形
.overlay {
Text("\(item)").font(.headline)
}
}
}
.padding()
}
// alignment:网格整体的水平对齐方式
LazyVGrid(columns: threeColumns, alignment: .leading, spacing: 16) {
// ...
}
LazyHGrid — 水平方向网格(向右扩展)
// 行规格(类似列但方向旋转)
let rows = [
GridItem(.fixed(80)),
GridItem(.fixed(80))
]
ScrollView(.horizontal) {
LazyHGrid(rows: rows, spacing: 12) {
ForEach(0..<20) { i in
RoundedRectangle(cornerRadius: 8)
.fill(Color.green.opacity(0.4))
.frame(width: 80)
.overlay {
Text("\(i)").font(.headline)
}
}
}
.padding()
}
3.5 ScrollView
ScrollView 允许内容超过屏幕尺寸时进行滚动。它本身不对子视图施加布局,需要配合 Stack 或 Grid 使用。
// 垂直滚动(默认)
ScrollView {
VStack(spacing: 12) {
ForEach(0..<50) { i in
Text("第 \(i) 行")
.frame(maxWidth: .infinity)
.padding()
.background(.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
// 水平滚动
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(0..<20) { i in
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue.opacity(0.2))
.frame(width: 120, height: 80)
.overlay { Text("卡片 \(i)") }
}
}
.padding(.horizontal)
}
// 同时支持两个方向滚动
ScrollView([.horizontal, .vertical]) {
// 超大内容区域
}
// iOS 17+ scrollPosition:记录/控制滚动位置
@State private var scrollPosition: Int? = nil
ScrollView {
LazyVStack {
ForEach(0..<100) { i in
Text("行 \(i)").id(i)
}
}
}
.scrollPosition(id: $scrollPosition)
Button("滚到第 50 行") {
withAnimation { scrollPosition = 50 }
}
3.6 GeometryReader
GeometryReader 是一个特殊的容器视图,它将父视图分配给自己的空间尺寸暴露给子视图,使子视图能基于父视图的实际大小来动态计算布局。
GeometryReader 会撑满父视图所有可用空间(贪婪布局),并且会引入额外的布局计算。只在真正需要获取父视图尺寸时使用,不要滥用。
// 获取父视图尺寸,制作宽度比例的进度条
GeometryReader { geometry in
// geometry.size.width — 可用宽度
// geometry.size.height — 可用高度
// geometry.frame(in: .global) — 在屏幕坐标中的 frame
// geometry.frame(in: .local) — 在自身坐标中的 frame
ZStack(alignment: .leading) {
Rectangle()
.fill(.gray.opacity(0.3))
.frame(height: 8)
Rectangle()
.fill(.blue)
.frame(width: geometry.size.width * 0.6, height: 8) // 60% 进度
}
.cornerRadius(4)
}
.frame(height: 8)
// 响应式图片:始终保持全宽
GeometryReader { geo in
Image(systemName: "photo")
.resizable()
.frame(width: geo.size.width, height: geo.size.width * 0.5625) // 16:9
}
3.7 安全区域(Safe Area)
iOS 设备有刘海(Dynamic Island)、Home Indicator 等物理特征,安全区域是屏幕上不被这些元素遮挡的区域。SwiftUI 默认让内容在安全区域内渲染。
// 让背景色延伸到安全区域(全屏背景)
ZStack {
Color.blue.ignoresSafeArea() // 背景扩展到边缘
Text("内容仍在安全区域内") // 文字不受影响
}
// 指定方向忽略安全区域
Color.green
.ignoresSafeArea(.container, edges: .bottom) // 仅底部
// safeAreaInset:在安全区域边缘插入额外视图(iOS 15+)
// 常用于在 ScrollView 底部悬浮一个按钮
ScrollView {
ForEach(0..<30) { i in
Text("内容 \(i)").padding()
}
}
.safeAreaInset(edge: .bottom) {
// 悬浮在底部的操作按钮,ScrollView 会自动为其留出空间
Button("发布") { }
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.foregroundColor(.white)
.padding(.horizontal)
.background(.regularMaterial) // 毛玻璃效果
}
3.8 实战示例:卡片网格布局
结合本章所有知识,构建一个常见的 App 首页卡片网格布局。
// 数据模型
struct AppItem: Identifiable {
let id = UUID()
let name: String
let icon: String // SF Symbol 名称
let color: Color
let description: String
}
// 单个卡片视图
struct AppCard: View {
let item: AppItem
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// 图标区域
ZStack {
RoundedRectangle(cornerRadius: 14)
.fill(item.color.opacity(0.15))
.frame(width: 52, height: 52)
Image(systemName: item.icon)
.font(.system(size: 24))
.foregroundColor(item.color)
}
// 名称
Text(item.name)
.font(.headline)
.lineLimit(1)
// 简介
Text(item.description)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
Spacer()
}
.padding()
.frame(maxWidth: .infinity, minHeight: 140, alignment: .topLeading)
.background(.regularMaterial)
.cornerRadius(16)
.shadow(color: .black.opacity(0.06), radius: 8, y: 4)
}
}
// 主页视图
struct HomeGridView: View {
let apps: [AppItem] = [
AppItem(name: "相机", icon: "camera.fill", color: .purple,
description: "拍摄高质量照片和视频"),
AppItem(name: "地图", icon: "map.fill", color: .green,
description: "导航与位置服务"),
AppItem(name: "音乐", icon: "music.note", color: .pink,
description: "海量音乐在线聆听"),
AppItem(name: "天气", icon: "cloud.sun.fill", color: .blue,
description: "实时天气预报"),
AppItem(name: "邮件", icon: "envelope.fill", color: .blue,
description: "收发电子邮件"),
AppItem(name: "备忘录", icon: "note.text", color: .yellow,
description: "记录灵感和待办事项"),
]
// 自适应列:每列至少 160pt
let columns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// 顶部横向滚动推荐区
Text("精选推荐").font(.title2).fontWeight(.bold)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(0..<5) { i in
RoundedRectangle(cornerRadius: 14)
.fill(LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 280, height: 160)
.overlay {
Text("Banner \(i + 1)")
.foregroundColor(.white)
.font(.headline)
}
}
}
.padding(.horizontal)
}
// 网格区域
Text("全部应用").font(.title2).fontWeight(.bold)
.padding(.horizontal)
LazyVGrid(columns: columns, spacing: 12) {
ForEach(apps) { app in
AppCard(item: app)
}
}
.padding(.horizontal)
}
.padding(.top)
}
// 悬浮底部按钮
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
Button {
// 添加应用
} label: {
Label("添加应用", systemImage: "plus.circle.fill")
.font(.headline)
.padding(.horizontal, 24)
.padding(.vertical, 14)
.background(.blue)
.foregroundColor(.white)
.clipShape(Capsule())
.shadow(radius: 8, y: 4)
}
Spacer()
}
.padding(.bottom, 8)
.background(.regularMaterial)
}
}
}
本章学习了 SwiftUI 布局系统的核心:HStack/VStack/ZStack 三种 Stack、Spacer 与 Divider 工具、padding 与 frame 的使用方式、LazyVGrid/LazyHGrid 网格布局、ScrollView 滚动容器、GeometryReader 动态尺寸获取,以及安全区域处理。下一章进入 SwiftUI 最重要的主题——状态管理。