swiftui-layout-components

swiftui-layout-components

热门

使用堆栈、网格、列表、滚动视图、表单和控件构建 SwiftUI 布局。涵盖 VStack/HStack/ZStack、LazyVGrid/LazyHGrid、带分区和滑动操作的 List、带 ScrollPosition 和滚动驱动揭示表面的 ScrollView、带验证的表单、Toggle/Picker/Slider、.searchable 以及覆盖层模式。适用于构建数据驱动布局、集合视图、分页详情展示、设置界面、搜索界面或临时覆盖层 UI。

931Star
0Fork
更新于 2026/7/26
SKILL.md
readonly只读
name
swiftui-layout-components
description

使用堆栈、网格、列表、滚动视图、表单和控件构建 SwiftUI 布局。涵盖 VStack/HStack/ZStack、LazyVGrid/LazyHGrid、带分区和滑动操作的 List、带 ScrollPosition 和滚动驱动揭示表面的 ScrollView、带验证的表单、Toggle/Picker/Slider、.searchable 以及覆盖层模式。适用于构建数据驱动布局、集合视图、分页详情展示、设置界面、搜索界面或临时覆盖层 UI。

SwiftUI 布局与组件

适用于 iOS 26+ 和 Swift 6.3 的 SwiftUI 应用布局和组件模式。涵盖堆栈和网格布局、列表模式、滚动视图、表单、控件、搜索和覆盖层。除非另有说明,模式向后兼容至 iOS 17。

目录

布局基础

标准堆栈

对于小型、固定大小的内容,使用 VStackHStackZStack。它们会立即渲染所有子视图。

VStack(alignment: .leading) {
    Text(title).font(.headline)
    Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}

惰性堆栈

ScrollView 内部使用 LazyVStackLazyHStack 处理大型或动态集合。它们会在子视图滚动到可见区域时按需创建。

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 技能处理。

常见错误

  1. 在惰性容器内放置 GeometryReader 会破坏惰性加载;需要尺寸时使用 .onGeometryChange
  2. 数组索引会导致不稳定的 ForEach ID 并产生错误的差异计算。
  3. 同轴嵌套滚动视图会产生手势冲突。
  4. 沉重、自定义或扩展的 List 行应放在 ScrollView + LazyVStack 中。
  5. 大型选项集应使用菜单或内联选择器样式,而非 .segmented
  6. 除非需要特定间距,否则省略堆栈/网格的 spacing: 以使用平台自适应默认值。
  7. 滚动揭示应使用一个归一化的进度值驱动,而非并行的布尔值或重复的拖动手势。
  8. 保持每帧滚动几何局部化,避免更改用于计算进度的几何。

审查清单

  • [ ] 大型或动态集合使用 LazyVStack/LazyHStack
  • [ ] 所有 ForEach 项目使用稳定的 Identifiable ID(非数组索引)
  • [ ] 惰性容器内无 GeometryReader
  • [ ] List 样式与上下文匹配(信息流用 .plain,设置用 .insetGrouped
  • [ ] 结构化输入界面使用 Form(非自定义堆栈)
  • [ ] .searchable 使用 .task(id:) 对输入进行防抖
  • [ ] 数据源支持下拉刷新时添加 .refreshable
  • [ ] 覆盖层使用过渡和自动消失计时器
  • [ ] 可点击行使用 .contentShape(Rectangle())
  • [ ] 表单中使用 @FocusState 管理键盘焦点
  • [ ] 除非需要特定值,否则省略堆栈/网格的 spacing:
  • [ ] 滚动驱动揭示使用一个归一化进度值,并将几何更新保持在狭窄子树中
  • [ ] 冲突的缩放/裁剪交互禁用滚动,离散可见性效果不驱动连续动画

参考资料