mcpField
S
swiftui-layout-components

swiftui-layout-components

github.com/dpearson2699设计与 UI1.2k4.3k 次安装13 周前更新
代码仓库

关于 swiftui-layout-components

面向 SwiftUI 开发者的布局与组件模式技能,涵盖列表、滚动视图、表单、搜索和覆盖层,并提供容器选择与常见错误指导。

主要功能

  1. 01栈与网格布局模式
  2. 02列表和滚动视图模式
  3. 03表单、控件与搜索
  4. 04覆盖层和呈现指导
  5. 05性能与常见错误检查

适用场景

  1. 01为大型集合选择惰性布局
  2. 02构建设置表单和输入界面
  3. 03为列表添加搜索与滚动行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-layout-components

安装后在 Claude Code 中输入/swiftui-layout-components确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
form.md
grids.md
list.md
scrollview.md

详细介绍

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 列来固定列数。

// Adaptive grid -- columns adjust to fit
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]

LazyVGrid(columns: columns) {
    ForEach(items) { item in
        ThumbnailView(item: item)
            .aspectRatio(1, contentMode: .fit)
    }
}
// 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)
    }
}

使用 .aspectRatio 设置单元格尺寸。切勿在延迟容器中放置 GeometryReader——它会强制进行急切测量,抵消延迟加载的效果。如果需要读取尺寸,请使用 .onGeometryChange(iOS 16+)。

完整的网格模式和设计选择请参阅 references/grids.md。

列表模式

当内置的行复用、选择和辅助功能很重要时,请使用 List 展示信息流式内容和设置行。

List {
    Section("General") {
        NavigationLink("Display") { DisplaySettingsView() }
        NavigationLink("Haptics") { HapticsSettingsView() }
    }
    Section("Account") {
        Button("Sign Out", 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

使用 Form 构建结构化设置和输入页面。将相关控件分组到 Section 区块中。

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)

使用 @FocusState 管理输入密集型表单中的键盘焦点。仅在独立展示或以工作表形式展示时,才使用 NavigationStack 包裹。

控件
控件用法
Toggle布尔偏好设置
Picker离散选项;对于 2-4 个选项,使用 .segmented
Slider带有可见数值标签的数值范围
DatePicker日期/时间选择
TextField文本输入,支持 .keyboardType、.textInputAutocapitalization

将控件直接绑定到 @State、@Binding 或 @AppStorage。使用 Form 分区对相关控件分组。使用 .disabled(...) 反映已锁定或继承的设置。如果有助于表达清晰,可在切换控件中使用 Label 组合图标和文本。

对于大量选项,避免使用 .pickerStyle(.segmented);改用菜单或内联样式。不要隐藏滑块标签;始终显示上下文。

完整表单示例请参阅 references/form.md。

可搜索界面

使用 .searchable 添加原生搜索界面。对于多种模式,使用 .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("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)
  }
}

搜索内容为空时显示占位提示。对输入进行防抖处理,避免过度请求。将搜索状态保留在视图本地。避免针对空字符串执行搜索。

覆盖层与展示

使用 .overlay(alignment:) 显示临时 UI(Toast、横幅),而不影响布局。

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)。除非确有需要,否则避免使用会阻止所有交互的覆盖层。不要叠加过多覆盖层;使用队列或替换当前 Toast。

对于模态路由、工作表停靠点和全屏展示策略,请交由 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:
  • 滚动驱动的显示效果使用单个归一化进度值,并将几何信息更新限制在较小的子树中
  • 缩放/裁剪交互冲突时禁用滚动;离散的可见性效果不驱动连续动画

参考资料

评论

支持 Markdown