使用堆栈、网格、列表、滚动视图、表单和控件构建 SwiftUI 布局。涵盖 VStack/HStack/ZStack、LazyVGrid/LazyHGrid、带分区和滑动操作的 List、带 ScrollPosition 和滚动驱动揭示表面的 ScrollView、带验证的表单、Toggle/Picker/Slider、.searchable 以及覆盖层模式。适用于构建数据驱动布局、集合视图、分页详情展示、设置界面、搜索界面或临时覆盖层 UI。
SwiftUI 布局与组件
适用于 iOS 26+ 和 Swift 6.3 的 SwiftUI 应用布局和组件模式。涵盖堆栈和网格布局、列表模式、滚动视图、表单、控件、搜索和覆盖层。除非另有说明,模式向后兼容至 iOS 17。
目录
布局基础
标准堆栈
对于小型、固定大小的内容,使用 VStack、HStack 和 ZStack。它们会立即渲染所有子视图。
VStack(alignment: .leading) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}
惰性堆栈
在 ScrollView 内部使用 LazyVStack 和 LazyHStack 处理大型或动态集合。它们会在子视图滚动到可见区域时按需创建。
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}
何时使用哪种:
- 非惰性堆栈: 小型、固定内容(标题、工具栏、字段少的表单)
- 惰性堆栈: 大型或未知大小的集合、信息流、聊天消息
网格布局
对于图标选择器、媒体画廊和密集的视觉选择,使用 LazyVGrid。使用 .adaptive 列实现跨设备尺寸自适应布局,或使用 .flexible 列实现固定列数。
// 自适应网格——列数自动调整
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns) {
ForEach(items) { item in
ThumbnailView(item: item)
.aspectRatio(1, contentMode: .fit)
}
}
// 固定3列网格
let columns = Array(repeating: GridItem(.flexible(minimum: 100), spacing: 4), count: 3)
LazyVGrid(columns: columns, spacing: 4) {
ForEach(items) { item in
ThumbnailView(item: item)
}
}
使用 .aspectRatio 控制单元格大小。切勿在惰性容器内放置 GeometryReader——它会强制进行急切测量并破坏惰性加载。如果需要读取尺寸,请使用 .onGeometryChange(iOS 16+)。
完整的网格模式和设计选择请参见 references/grids.md。
列表模式
对于信息流式内容和设置行,使用 List,因为它内置了行复用、选择和辅助功能。
List {
Section("通用") {
NavigationLink("显示") { DisplaySettingsView() }
NavigationLink("触觉") { HapticsSettingsView() }
}
Section("账户") {
Button("退出登录", role: .destructive) { }
}
}
.listStyle(.insetGrouped)
关键模式:
- 信息流布局使用
.listStyle(.plain),设置界面使用.insetGrouped - 对于主题化表面,使用
.scrollContentBackground(.hidden)+ 自定义背景 - 使用
.listRowInsets(...)和.listRowSeparator(.hidden)控制间距和分隔线 - 边缘滚动: 使用
List+ScrollPosition配合.scrollPosition($scrollPosition)实现顶部/底部滚动操作 - 项目或分区跳转: 使用
ScrollView+ 惰性堆栈配合.scrollTargetLayout()和稳定目标,实现可靠的跳转到 ID 行为 - 使用
.refreshable { }实现下拉刷新信息流 - 在需要整行可点击的行上使用
.contentShape(Rectangle()) - 对于布局审查或迁移指导,首先选择容器和约束;保持代码片段简短;将弹簧、过渡和时序选择交给
swiftui-animation
iOS 26: 应用 .scrollEdgeEffectStyle(.soft, for: .top) 实现现代滚动边缘效果。
完整的列表模式(包括带滚动到顶部的信息流列表)请参见 references/list.md。
ScrollView
当需要自定义布局、混合内容或水平滚动时,使用 ScrollView 配合惰性堆栈。
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
ScrollPosition: 支持声明式、双向滚动位置跟踪和编程式滚动。
@State private var scrollPosition = ScrollPosition(edge: .bottom)
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
}
}
.scrollTargetLayout()
}
.scrollPosition($scrollPosition)
.onChange(of: messages.last?.id) {
withAnimation { scrollPosition.scrollTo(edge: .bottom) }
}
safeAreaInset(edge:) 将内容(输入栏、工具栏)固定在键盘上方,不影响滚动布局。
iOS 26 新增:
.scrollEdgeEffectStyle(.soft, for: .top)—— 淡出边缘效果.backgroundExtensionEffect()—— 在安全区域边缘镜像/模糊(谨慎使用,每屏一个).safeAreaBar(edge:)—— 附加与滚动效果集成的栏视图
完整的 ScrollPosition、分页揭示、缩放/裁剪冲突和 iOS 26 边缘效果模式请参见 references/scrollview.md。
表单与控件
表单
对于结构化的设置和输入界面,使用 Form。将相关控件分组到 Section 块中。
Form {
Section("通知") {
Toggle("提及", isOn: $prefs.mentions)
Toggle("关注", isOn: $prefs.follows)
}
Section("外观") {
Picker("主题", selection: $theme) {
ForEach(Theme.allCases, id: \.self) { Text($0.title).tag($0) }
}
Slider(value: $fontScale, in: 0.5...1.5, step: 0.1)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
在输入密集的表单中使用 @FocusState 管理键盘焦点。仅在独立呈现或位于 sheet 中时包裹在 NavigationStack 中。
控件
| 控件 | 用途 |
|---|---|
Toggle |
布尔偏好设置 |
Picker |
离散选择;2-4 个选项使用 .segmented |
Slider |
数值范围,带可见值标签 |
DatePicker |
日期/时间选择 |
TextField |
文本输入,配合 .keyboardType、.textInputAutocapitalization |
将控件直接绑定到 @State、@Binding 或 @AppStorage。在 Form 分区中对相关控件进行分组。使用 .disabled(...) 反映锁定或继承的设置。在切换开关中使用 Label 组合图标和文本,以增加清晰度。
对于大型选项集,避免使用 .pickerStyle(.segmented);改用菜单或内联样式。不要隐藏滑块的标签;始终显示上下文。
完整的表单示例请参见 references/form.md。
可搜索
使用 .searchable 添加原生搜索 UI。使用 .searchScopes 实现多种模式,使用 .task(id:) 实现防抖异步结果。
@MainActor
struct ExploreView: View {
@State private var searchQuery = ""
@State private var searchScope: SearchScope = .all
@State private var isSearching = false
@State private var results: [SearchResult] = []
var body: some View {
List {
if isSearching {
ProgressView()
} else {
ForEach(results) { result in
SearchRow(result: result)
}
}
}
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("搜索")
)
.searchScopes($searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.title)
}
}
.task(id: searchQuery) {
await runSearch()
}
}
private func runSearch() async {
guard !searchQuery.isEmpty else {
results = []
return
}
isSearching = true
defer { isSearching = false }
try? await Task.sleep(for: .milliseconds(250))
results = await fetchResults(query: searchQuery, scope: searchScope)
}
}
搜索为空时显示占位符。对输入进行防抖以避免过度获取。将搜索状态保持在视图本地。避免对空字符串执行搜索。
覆盖层与呈现
使用 .overlay(alignment:) 实现不影响布局的临时 UI(提示、横幅)。
struct AppRootView: View {
@State private var toast: Toast?
var body: some View {
content
.overlay(alignment: .top) {
if let toast {
ToastView(toast: toast)
.transition(.move(edge: .top).combined(with: .opacity))
.onAppear {
Task {
try? await Task.sleep(for: .seconds(2))
withAnimation { self.toast = nil }
}
}
}
}
}
}
对于临时 UI,优先使用覆盖层而非嵌入布局堆栈。使用过渡和短时自动消失计时器。将覆盖层对齐到清晰的边缘(.top 或 .bottom)。除非明确需要,否则避免使用阻止所有交互的覆盖层。不要堆叠多个覆盖层;使用队列或替换当前提示。
对于模态路由、sheet 卡位和全屏呈现策略,请交给 swiftui-navigation 技能处理。
常见错误
- 在惰性容器内放置
GeometryReader会破坏惰性加载;需要尺寸时使用.onGeometryChange。 - 数组索引会导致不稳定的
ForEachID 并产生错误的差异计算。 - 同轴嵌套滚动视图会产生手势冲突。
- 沉重、自定义或扩展的
List行应放在ScrollView+LazyVStack中。 - 大型选项集应使用菜单或内联选择器样式,而非
.segmented。 - 除非需要特定间距,否则省略堆栈/网格的
spacing:以使用平台自适应默认值。 - 滚动揭示应使用一个归一化的进度值驱动,而非并行的布尔值或重复的拖动手势。
- 保持每帧滚动几何局部化,避免更改用于计算进度的几何。
审查清单
- [ ] 大型或动态集合使用
LazyVStack/LazyHStack - [ ] 所有
ForEach项目使用稳定的IdentifiableID(非数组索引) - [ ] 惰性容器内无
GeometryReader - [ ]
List样式与上下文匹配(信息流用.plain,设置用.insetGrouped) - [ ] 结构化输入界面使用
Form(非自定义堆栈) - [ ]
.searchable使用.task(id:)对输入进行防抖 - [ ] 数据源支持下拉刷新时添加
.refreshable - [ ] 覆盖层使用过渡和自动消失计时器
- [ ] 可点击行使用
.contentShape(Rectangle()) - [ ] 表单中使用
@FocusState管理键盘焦点 - [ ] 除非需要特定值,否则省略堆栈/网格的
spacing: - [ ] 滚动驱动揭示使用一个归一化进度值,并将几何更新保持在狭窄子树中
- [ ] 冲突的缩放/裁剪交互禁用滚动,离散可见性效果不驱动连续动画
参考资料
- 网格模式:references/grids.md
- 列表和分区模式:references/list.md
- ScrollView 和惰性堆栈:references/scrollview.md
- 表单模式:references/form.md
- 架构和状态管理:参见
swiftui-patterns技能 - 导航模式:参见
swiftui-navigation技能






