swiftui-layout-components
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Layout & Components
SwiftUI 布局与组件
Layout and component patterns for SwiftUI apps targeting iOS 26+ with Swift 6.2. Covers stack and grid layouts, list patterns, scroll views, forms, controls, search, and overlays. Patterns are backward-compatible to iOS 17 unless noted.
针对iOS 26+、使用Swift 6.2开发的SwiftUI应用的布局与组件模式。涵盖栈布局、网格布局、列表模式、滚动视图、表单、控件、搜索和覆盖层。除非特别说明,所有模式均向下兼容至iOS 17。
Contents
目录
Layout Fundamentals
布局基础
Standard Stacks
标准栈
Use , , and for small, fixed-size content. They render all children immediately.
VStackHStackZStackswift
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}针对小型固定尺寸内容使用、和,它们会立即渲染所有子元素。
VStackHStackZStackswift
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}Lazy Stacks
懒加载栈
Use and inside for large or dynamic collections. They create child views on demand as they scroll into view.
LazyVStackLazyHStackScrollViewswift
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}When to use which:
- Non-lazy stacks: Small, fixed content (headers, toolbars, forms with few fields)
- Lazy stacks: Large or unknown-size collections, feeds, chat messages
在内部针对大型或动态集合使用和,它们会在元素滚动进入可视区域时按需创建子视图。
ScrollViewLazyVStackLazyHStackswift
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}适用场景区分:
- 非懒加载栈: 小型固定内容(头部、工具栏、字段较少的表单)
- 懒加载栈: 大型或尺寸未知的集合、信息流、聊天消息
Grid Layouts
网格布局
Use for icon pickers, media galleries, and dense visual selections. Use columns for layouts that scale across device sizes, or columns for a fixed column count.
LazyVGrid.adaptive.flexibleswift
// Adaptive grid -- columns adjust to fit
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns, spacing: 6) {
ForEach(items) { item in
ThumbnailView(item: item)
.aspectRatio(1, contentMode: .fit)
}
}swift
// Fixed 3-column grid
let columns = Array(repeating: GridItem(.flexible(minimum: 100), spacing: 4), count: 3)
LazyVGrid(columns: columns, spacing: 4) {
ForEach(items) { item in
ThumbnailView(item: item)
}
}Use for cell sizing. Never place inside lazy containers -- it forces eager measurement and defeats lazy loading. Use (iOS 18+) if you need to read dimensions.
.aspectRatioGeometryReader.onGeometryChangeSee for full grid patterns and design choices.
references/grids.md针对图标选择器、媒体画廊和密集视觉选择场景使用。使用列实现适配不同设备尺寸的布局,或使用列实现固定列数的布局。
LazyVGrid.adaptive.flexibleswift
// 自适应网格 -- 列数自动调整适配空间
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns, spacing: 6) {
ForEach(items) { item in
ThumbnailView(item: item)
.aspectRatio(1, contentMode: .fit)
}
}swift
// 固定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)
}
}使用设置单元格尺寸。绝对不要在懒加载容器内部放置——它会强制提前测量,破坏懒加载机制。如果需要读取尺寸,请使用(iOS 18+)。
.aspectRatioGeometryReader.onGeometryChange完整网格模式和设计选择请查看。
references/grids.mdList Patterns
列表模式
Use for feed-style content and settings rows where built-in row reuse, selection, and accessibility matter.
Listswift
List {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Sign Out", role: .destructive) { }
}
}
.listStyle(.insetGrouped)Key patterns:
- for feed layouts,
.listStyle(.plain)for settings.insetGrouped - + custom background for themed surfaces
.scrollContentBackground(.hidden) - and
.listRowInsets(...)for spacing and separator control.listRowSeparator(.hidden) - Pair with for scroll-to-top or jump-to-id
ScrollViewReader - Use for pull-to-refresh feeds
.refreshable { } - Use on rows that should be tappable end-to-end
.contentShape(Rectangle())
iOS 26: Apply for modern scroll edge effects.
.scrollEdgeEffectStyle(.soft, for: .top)See for full list patterns including feed lists with scroll-to-top.
references/list.md针对信息流风格内容和设置行使用,这类场景需要内置的行复用、选择能力和无障碍支持。
Listswift
List {
Section("通用") {
NavigationLink("显示") { DisplaySettingsView() }
NavigationLink("触感反馈") { HapticsSettingsView() }
}
Section("账户") {
Button("退出登录", role: .destructive) { }
}
}
.listStyle(.insetGrouped)核心模式:
- 信息流布局使用,设置页面使用
.listStyle(.plain).insetGrouped - 自定义主题背景时搭配使用
.scrollContentBackground(.hidden) - 使用和
.listRowInsets(...)控制间距和分割线.listRowSeparator(.hidden) - 配合实现滚动到顶部或跳转到指定ID的功能
ScrollViewReader - 使用实现信息流下拉刷新
.refreshable { } - 需要整行可点击时为行元素添加
.contentShape(Rectangle())
iOS 26特性: 添加实现现代化滚动边缘效果。
.scrollEdgeEffectStyle(.soft, for: .top)完整列表模式(包括支持滚动到顶部的信息流列表)请查看。
references/list.mdScrollView
ScrollView
Use with lazy stacks when you need custom layout, mixed content, or horizontal scrolling.
ScrollViewswift
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}ScrollViewReader: Enables programmatic scrolling to specific items.
swift
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message).id(message.id)
}
}
}
.onChange(of: messages.last?.id) { _, newValue in
if let id = newValue {
withAnimation { proxy.scrollTo(id, anchor: .bottom) }
}
}
}safeAreaInset(edge:)iOS 26 additions:
- -- fading edge effect
.scrollEdgeEffectStyle(.soft, for: .top) - -- mirror/blur at safe area edges (use sparingly, one per screen)
.backgroundExtensionEffect() - -- attach bar views that integrate with scroll effects
.safeAreaBar(edge:)
See for full scroll patterns and iOS 26 edge effects.
references/scrollview.md当你需要自定义布局、混合内容或横向滚动时,配合懒加载栈使用。
ScrollViewswift
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}ScrollViewReader: 支持编程式滚动到指定元素。
swift
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message).id(message.id)
}
}
}
.onChange(of: messages.last?.id) { _, newValue in
if let id = newValue {
withAnimation { proxy.scrollTo(id, anchor: .bottom) }
}
}
}safeAreaInset(edge:)iOS 26新增特性:
- -- 边缘渐变效果
.scrollEdgeEffectStyle(.soft, for: .top) - -- 安全区域边缘的镜像/模糊效果(谨慎使用,每个页面最多一个)
.backgroundExtensionEffect() - -- 附加可与滚动效果联动的栏视图
.safeAreaBar(edge:)
完整滚动模式和iOS 26边缘效果请查看。
references/scrollview.mdForm and Controls
表单与控件
Form
表单
Use for structured settings and input screens. Group related controls into blocks.
FormSectionswift
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $prefs.mentions)
Toggle("Follows", isOn: $prefs.follows)
}
Section("Appearance") {
Picker("Theme", 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)Use to manage keyboard focus in input-heavy forms. Wrap in only when presented standalone or in a sheet.
@FocusStateNavigationStack针对结构化设置和输入页面使用,将相关控件分组到块中。
FormSectionswift
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)在输入密集的表单中使用管理键盘焦点。仅当独立展示或在弹窗中展示时才用包裹。
@FocusStateNavigationStackControls
控件
| Control | Usage |
|---|---|
| Boolean preferences |
| Discrete choices; |
| Numeric ranges with visible value label |
| Date/time selection |
| Text input with |
Bind controls directly to , , or . Group related controls in sections. Use to reflect locked or inherited settings. Use inside toggles to combine icon + text when it adds clarity.
@State@Binding@AppStorageForm.disabled(...)Labelswift
// Toggle sections
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $preferences.notificationsMentionsEnabled)
Toggle("Follows", isOn: $preferences.notificationsFollowsEnabled)
}
}
// Slider with value text
Section("Font Size") {
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
Text("Scale: \(String(format: "%.1f", fontSizeScale))")
}
// Picker for enums
Picker("Default Visibility", selection: $visibility) {
ForEach(Visibility.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}Avoid for large sets; use menu or inline styles. Don't hide labels for sliders; always show context.
.pickerStyle(.segmented)See for full form examples.
references/form.md| 控件 | 用法 |
|---|---|
| 布尔类型偏好设置 |
| 离散选择;2-4个选项时使用 |
| 数值范围选择,需搭配可见的数值标签 |
| 日期/时间选择 |
| 文本输入,搭配 |
将控件直接绑定到、或。将相关控件分组到的分区中。使用标识锁定或继承的设置。如果需要提升清晰度,可以在Toggle中使用组合图标+文本。
@State@Binding@AppStorageForm.disabled(...)Labelswift
// Toggle分区
Form {
Section("通知") {
Toggle("@提醒", isOn: $preferences.notificationsMentionsEnabled)
Toggle("关注动态", isOn: $preferences.notificationsFollowsEnabled)
}
}
// 带数值显示的Slider
Section("字体大小") {
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
Text("缩放比例: \(String(format: "%.1f", fontSizeScale))")
}
// 枚举类型Picker
Picker("默认可见性", selection: $visibility) {
ForEach(Visibility.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}选项较多时避免使用,改用菜单或行内样式。不要隐藏Slider的标签,始终展示上下文说明。
.pickerStyle(.segmented)完整表单示例请查看。
references/form.mdSearchable
Searchable
Add native search UI with . Use for multiple modes and for debounced async results.
.searchable.searchScopes.task(id:)swift
@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("Search")
)
.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)
}
}Show a placeholder when search is empty. Debounce input to avoid overfetching. Keep search state local to the view. Avoid running searches for empty strings.
使用添加原生搜索UI。使用实现多模式搜索,使用实现防抖异步请求。
.searchable.searchScopes.task(id:)swift
@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 and Presentation
覆盖层与弹窗
Use for transient UI (toasts, banners) without affecting layout.
.overlay(alignment:)swift
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 }
}
}
}
}
}
}Prefer overlays for transient UI rather than embedding in layout stacks. Use transitions and short auto-dismiss timers. Keep overlays aligned to a clear edge ( or ). Avoid overlays that block all interaction unless explicitly needed. Don't stack many overlays; use a queue or replace the current toast.
.top.bottomfullScreenCover: Use for immersive presentations that cover the entire screen (media viewers, onboarding flows).
.fullScreenCover(item:)使用实现临时UI(toast提示、横幅),不会影响原有布局。
.overlay(alignment:)swift
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优先使用覆盖层实现,不要嵌入布局栈中。使用转场动画和短时间自动消失计时器。将覆盖层对齐到清晰的边缘(或)。除非明确需要,避免使用阻断所有交互的覆盖层。不要堆叠多个覆盖层,使用队列或者替换当前toast。
.top.bottomfullScreenCover: 使用实现覆盖整个屏幕的沉浸式弹窗(媒体查看器、新用户引导流程)。
.fullScreenCover(item:)Common Mistakes
常见错误
- Using non-lazy stacks for large collections -- causes all children to render immediately
- Placing inside lazy containers -- defeats lazy loading
GeometryReader - Using array indices as IDs -- causes incorrect diffing and UI bugs
ForEach - Nesting scroll views of the same axis -- causes gesture conflicts
- Heavy custom layouts inside rows -- use
List+ScrollViewinsteadLazyVStack - Missing on tappable rows -- tap area is text-only
.contentShape(Rectangle()) - Hard-coding frame dimensions for sheets -- use instead
.presentationSizing - Running searches on empty strings -- always guard against empty queries
- Mixing and
Listin the same hierarchy -- gesture conflictsScrollView - Using for large option sets -- use menu or inline styles
.pickerStyle(.segmented)
- 对大型集合使用非懒加载栈——会导致所有子元素立即渲染
- 在懒加载容器内部放置——破坏懒加载机制
GeometryReader - 使用数组索引作为的ID——导致差异计算错误和UI bug
ForEach - 嵌套同轴向的滚动视图——导致手势冲突
- 在行内部使用复杂自定义布局——改用
List+ScrollViewLazyVStack - 可点击行未添加——只有文本区域可点击
.contentShape(Rectangle()) - 为弹窗硬编码尺寸——改用
.presentationSizing - 为空字符串执行搜索请求——始终做空查询拦截
- 在同一层级混合使用和
List——导致手势冲突ScrollView - 选项较多时使用——改用菜单或行内样式
.pickerStyle(.segmented)
Review Checklist
审核清单
- /
LazyVStackused for large or dynamic collectionsLazyHStack - Stable IDs on all
Identifiableitems (not array indices)ForEach - No inside lazy containers
GeometryReader - style matches context (
Listfor feeds,.plainfor settings).insetGrouped - used for structured input screens (not custom stacks)
Form - debounces input with
.searchable.task(id:) - added where data source supports pull-to-refresh
.refreshable - Overlays use transitions and auto-dismiss timers
- on tappable rows
.contentShape(Rectangle()) - manages keyboard focus in forms
@FocusState
- 大型或动态集合使用了/
LazyVStackLazyHStack - 所有元素使用了稳定的
ForEachID(不是数组索引)Identifiable - 懒加载容器内部没有
GeometryReader - 样式匹配场景(信息流用
List,设置页用.plain).insetGrouped - 结构化输入页面使用了(不是自定义栈)
Form - 通过
.searchable实现了输入防抖.task(id:) - 数据源支持下拉刷新的场景添加了
.refreshable - 覆盖层使用了转场动画和自动消失计时器
- 可点击行添加了
.contentShape(Rectangle()) - 表单中使用管理键盘焦点
@FocusState
References
参考资料
- Grid patterns:
references/grids.md - List and section patterns:
references/list.md - ScrollView and lazy stacks:
references/scrollview.md - Form patterns:
references/form.md - Architecture and state management: see skill
swiftui-patterns - Navigation patterns: see skill
swiftui-navigation
- 网格模式:
references/grids.md - 列表与分区模式:
references/list.md - ScrollView与懒加载栈:
references/scrollview.md - 表单模式:
references/form.md - 架构与状态管理: 查看技能
swiftui-patterns - 导航模式: 查看技能
swiftui-navigation