swiftui-layout-components by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftui-layout-components适用于 iOS 26+ 和 Swift 6.2 的 SwiftUI 应用布局与组件模式。涵盖堆栈和网格布局、列表模式、滚动视图、表单、控件、搜索和覆盖层。除非特别说明,这些模式向后兼容至 iOS 17。
对于小型、固定大小的内容,使用 VStack、HStack 和 ZStack。它们会立即渲染所有子视图。
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}
在 ScrollView 内部使用 LazyVStack 和 来处理大型或动态集合。它们会在子视图滚动进入视野时按需创建。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
LazyHStackScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}
何时使用哪种:
对于图标选择器、媒体库和密集的可视化选择,使用 LazyVGrid。对于需要跨设备尺寸缩放的布局,使用 .adaptive 列;对于固定列数,使用 .flexible 列。
// 自适应网格 -- 列会根据空间调整
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns, spacing: 6) {
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 18+)。
完整的网格模式和设计选择,请参阅 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) 控制间距和分隔线ScrollViewReader 配对使用,实现滚动到顶部或跳转到特定 ID.refreshable { } 实现下拉刷新.contentShape(Rectangle())iOS 26: 应用 .scrollEdgeEffectStyle(.soft, for: .top) 以获得现代的滚动边缘效果。
完整的列表模式,包括带滚动到顶部功能的信息流列表,请参阅 references/list.md。
当需要自定义布局、混合内容或水平滚动时,使用 ScrollView 配合惰性堆栈。
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
ScrollViewReader: 支持以编程方式滚动到特定项目。
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.md。
对于结构化的设置和输入屏幕,使用 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 | 离散选择;.segmented 适用于 2-4 个选项 |
Slider | 带有可见值标签的数值范围 |
DatePicker | 日期/时间选择 |
TextField | 文本输入,可配合 .keyboardType、.textInputAutocapitalization 使用 |
将控件直接绑定到 @State、@Binding 或 @AppStorage。将相关控件分组到 Form 的 Section 中。使用 .disabled(...) 来反映锁定或继承的设置。在开关内部使用 Label 来组合图标和文本,以增加清晰度。
// 开关分组
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $preferences.notificationsMentionsEnabled)
Toggle("Follows", isOn: $preferences.notificationsFollowsEnabled)
}
}
// 带数值文本的滑块
Section("Font Size") {
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
Text("Scale: \(String(format: "%.1f", fontSizeScale))")
}
// 枚举选择器
Picker("Default Visibility", selection: $visibility) {
ForEach(Visibility.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}
对于大型选项集,避免使用 .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("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(如提示、横幅)。
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)。避免使用会阻塞所有交互的覆盖层,除非明确需要。不要堆叠多个覆盖层;使用队列或替换当前的提示。
fullScreenCover: 对于覆盖整个屏幕的沉浸式呈现(如媒体查看器、引导流程),使用 .fullScreenCover(item:)。
GeometryReader 放在惰性容器内部——破坏惰性加载ForEach 的 ID——导致错误的差异比较和 UI 错误List 行内部使用繁重的自定义布局——改用 ScrollView + LazyVStack.contentShape(Rectangle())——点击区域仅限于文本.presentationSizingList 和 ScrollView——手势冲突.pickerStyle(.segmented)——改用菜单或内联样式LazyVStack/LazyHStackForEach 项目都有稳定的 Identifiable ID(非数组索引)GeometryReaderList 样式符合上下文(信息流用 .plain,设置用 .insetGrouped)Form(而非自定义堆栈).searchable 使用 .task(id:) 对输入进行防抖.refreshable.contentShape(Rectangle())@FocusState 管理键盘焦点references/grids.mdreferences/list.mdreferences/scrollview.mdreferences/form.mdswiftui-patterns 技能swiftui-navigation 技能每周安装量
354
仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex351
cursor348
amp348
cline348
github-copilot348
kimi-cli348
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.
Use VStack, HStack, and ZStack for small, fixed-size content. They render all children immediately.
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}
Use LazyVStack and LazyHStack inside ScrollView for large or dynamic collections. They create child views on demand as they scroll into view.
ScrollView {
LazyVStack(spacing: 12) {
ForEach(items) { item in
ItemRow(item: item)
}
}
.padding(.horizontal)
}
When to use which:
Use LazyVGrid for icon pickers, media galleries, and dense visual selections. Use .adaptive columns for layouts that scale across device sizes, or .flexible columns for a fixed column count.
// 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)
}
}
// 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 .aspectRatio for cell sizing. Never place GeometryReader inside lazy containers -- it forces eager measurement and defeats lazy loading. Use .onGeometryChange (iOS 18+) if you need to read dimensions.
See references/grids.md for full grid patterns and design choices.
Use List for feed-style content and settings rows where built-in row reuse, selection, and accessibility matter.
List {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Sign Out", role: .destructive) { }
}
}
.listStyle(.insetGrouped)
Key patterns:
.listStyle(.plain) for feed layouts, .insetGrouped for settings.scrollContentBackground(.hidden) + custom background for themed surfaces.listRowInsets(...) and .listRowSeparator(.hidden) for spacing and separator controlScrollViewReader for scroll-to-top or jump-to-id.refreshable { } for pull-to-refresh feeds.contentShape(Rectangle()) on rows that should be tappable end-to-endiOS 26: Apply .scrollEdgeEffectStyle(.soft, for: .top) for modern scroll edge effects.
See references/list.md for full list patterns including feed lists with scroll-to-top.
Use ScrollView with lazy stacks when you need custom layout, mixed content, or horizontal scrolling.
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
ScrollViewReader: Enables programmatic scrolling to specific items.
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:) pins content (input bars, toolbars) above the keyboard without affecting scroll layout.
iOS 26 additions:
.scrollEdgeEffectStyle(.soft, for: .top) -- fading edge effect.backgroundExtensionEffect() -- mirror/blur at safe area edges (use sparingly, one per screen).safeAreaBar(edge:) -- attach bar views that integrate with scroll effectsSee references/scrollview.md for full scroll patterns and iOS 26 edge effects.
Use Form for structured settings and input screens. Group related controls into Section blocks.
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 @FocusState to manage keyboard focus in input-heavy forms. Wrap in NavigationStack only when presented standalone or in a sheet.
| Control | Usage |
|---|---|
Toggle | Boolean preferences |
Picker | Discrete choices; .segmented for 2-4 options |
Slider | Numeric ranges with visible value label |
DatePicker | Date/time selection |
TextField | Text input with .keyboardType, |
Bind controls directly to @State, @Binding, or @AppStorage. Group related controls in Form sections. Use .disabled(...) to reflect locked or inherited settings. Use Label inside toggles to combine icon + text when it adds clarity.
// 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 .pickerStyle(.segmented) for large sets; use menu or inline styles. Don't hide labels for sliders; always show context.
See references/form.md for full form examples.
Add native search UI with .searchable. Use .searchScopes for multiple modes and .task(id:) for debounced async results.
@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.
Use .overlay(alignment:) for transient UI (toasts, banners) without affecting layout.
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 (.top or .bottom). Avoid overlays that block all interaction unless explicitly needed. Don't stack many overlays; use a queue or replace the current toast.
fullScreenCover: Use .fullScreenCover(item:) for immersive presentations that cover the entire screen (media viewers, onboarding flows).
GeometryReader inside lazy containers -- defeats lazy loadingForEach IDs -- causes incorrect diffing and UI bugsList rows -- use ScrollView + LazyVStack instead.contentShape(Rectangle()) on tappable rows -- tap area is text-only.presentationSizing insteadList and ScrollView in the same hierarchy -- gesture conflictsLazyVStack/LazyHStack used for large or dynamic collectionsIdentifiable IDs on all ForEach items (not array indices)GeometryReader inside lazy containersList style matches context (.plain for feeds, .insetGrouped for settings)Form used for structured input screens (not custom stacks).searchable debounces input with references/grids.mdreferences/list.mdreferences/scrollview.mdreferences/form.mdswiftui-patterns skillswiftui-navigation skillWeekly Installs
354
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex351
cursor348
amp348
cline348
github-copilot348
kimi-cli348
腾讯云开发 UI 设计技能 - 专业前端界面设计与高保真原型开发指南
808 周安装
Python类型注解模式指南:现代类型提示与Typing最佳实践
24 周安装
Web应用安全模式指南:OWASP Top 10防护、输入验证、身份认证与授权最佳实践
25 周安装
task-runner任务运行器:使用just简化项目命令执行,替代make的跨平台工具
30 周安装
EdgeOne Pages 一键部署:无需账户,秒级将HTML文件发布到公共URL
35 周安装
Vibe Security 安全扫描器 - 多语言代码漏洞检测与AI智能修复工具
38 周安装
wechat-publisher:一键发布Markdown文章到微信公众号草稿箱工具
323 周安装
.textInputAutocapitalization.pickerStyle(.segmented) for large option sets -- use menu or inline styles.task(id:).refreshable added where data source supports pull-to-refresh.contentShape(Rectangle()) on tappable rows@FocusState manages keyboard focus in forms