Generates SwiftUI code for stacks, grids, lists, forms, and overlays—with working Toggle, Picker, and Slider components. Handles sections, validation, and search.
Best for: iOS developers building data-driven screens without wrestling layout syntax.
---
name: swiftui-layout-components
description: "Build SwiftUI layouts using stacks, grids, lists, scroll views, forms, and controls. Covers VStack/HStack/ZStack, LazyVGrid/LazyHGrid, List with sections and swipe actions, ScrollView with ScrollPosition, Form with validation, Toggle/Picker/Slider, .searchable, and overlay patterns. Use when building data-driven layouts, collection views, settings screens, search interfaces, or transient overlay UI."
---
# SwiftUI Layout & Components
Layout and component patterns for SwiftUI apps targeting iOS 26+ with Swift 6.3. Covers stack and grid layouts, list patterns, scroll views, forms, controls, search, and overlays. Patterns are backward-compatible to iOS 17 unless noted.
## Contents
- [Layout Fundamentals](#layout-fundamentals)
- [Grid Layouts](#grid-layouts)
- [List Patterns](#list-patterns)
- [ScrollView](#scrollview)
- [Form and Controls](#form-and-controls)
- [Searchable](#searchable)
- [Overlay and Presentation](#overlay-and-presentation)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Layout Fundamentals
### Standard Stacks
Use `VStack`, `HStack`, and `ZStack` for small, fixed-size content. They render all children immediately.
```swift
VStack(alignment: .leading) {
Text(title).font(.headline)
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}
```
### Lazy Stacks
Use `LazyVStack` and `LazyHStack` inside `ScrollView` for large or dynamic collections. They create child views on demand as they scroll into view.
```swift
ScrollView {
LazyVStack {
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
## Grid Layouts
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.
```swift
// 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)
}
}
```
```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 `.aspectRatio` for cell sizing. Never place `GeometryReader` inside lazy containers -- it forces eager measurement and defeats lazy loading. Use `.onGeometryChange` (iOS 16+) if you need to read dimensions.
See [references/grids.md](references/grids.md) for full grid patterns and design choices.
## List Patterns
Use `List` for feed-style content and settings rows where built-in row reuse, selection, and accessibility matter.
```swift
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 control
- Use `ScrollPosition` with `.scrollPosition($scrollPosition)` for scroll-to-top or jump-to-id
- Use `.refreshable { }` for pull-to-refresh feeds
- Use `.contentShape(Rectangle())` on rows that should be tappable end-to-end
**iOS 26:** Apply `.scrollEdgeEffectStyle(.soft, for: .top)` for modern scroll edge effects.
See [references/list.md](references/list.md) for full list patterns including feed lists with scroll-to-top.
## ScrollView
Use `ScrollView` with lazy stacks when you need custom layout, mixed content, or horizontal scrolling.
```swift
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
```
**ScrollPosition:** Enables declarative, bidirectional scroll position tracking and programmatic scrolling.
```swift
@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) }
}
```
See [references/scrollview.md](references/scrollview.md) for full `ScrollPosition` patterns including scroll-to-id and user-scroll detection.
**`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 effects
See [references/scrollview.md](references/scrollview.md) for full scroll patterns and iOS 26 edge effects.
## Form and Controls
### Form
Use `Form` for structured settings and input screens. Group related controls into `Section` blocks.
```swift
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.
### Controls
| 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`, `.textInputAutocapitalization` |
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.
```swift
// 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](references/form.md) for full form examples.
## Searchable
Add native search UI with `.searchable`. Use `.searchScopes` for multiple modes and `.task(id:)` for debounced async results.
```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.
## Overlay and Presentation
Use `.overlay(alignment:)` for transient UI (toasts, banners) without affecting layout.
```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 (`.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).
## Common Mistakes
1. Using non-lazy stacks for large collections -- causes all children to render immediately
2. Placing `GeometryReader` inside lazy containers -- defeats lazy loading
3. Using array indices as `ForEach` IDs -- causes incorrect diffing and UI bugs
4. Nesting scroll views of the same axis -- causes gesture conflicts
5. Heavy custom layouts inside `List` rows -- use `ScrollView` + `LazyVStack` instead
6. Missing `.contentShape(Rectangle())` on tappable rows -- tap area is text-only
7. Hard-coding frame dimensions for sheets -- use `.presentationSizing` instead
8. Running searches on empty strings -- always guard against empty queries
9. Mixing `List` and `ScrollView` in the same hierarchy -- gesture conflicts
10. Using `.pickerStyle(.segmented)` for large option sets -- use menu or inline styles
11. Hard-coding `spacing:` on stacks and grids by default -- omit to get platform-adaptive spacing; only specify for intentional tight (0–4pt) or wide gaps
## Review Checklist
- [ ] `LazyVStack`/`LazyHStack` used for large or dynamic collections
- [ ] Stable `Identifiable` IDs on all `ForEach` items (not array indices)
- [ ] No `GeometryReader` inside lazy containers
- [ ] `List` style matches context (`.plain` for feeds, `.insetGrouped` for settings)
- [ ] `Form` used for structured input screens (not custom stacks)
- [ ] `.searchable` debounces input with `.task(id:)`
- [ ] `.refreshable` added where data source supports pull-to-refresh
- [ ] Overlays use transitions and auto-dismiss timers
- [ ] `.contentShape(Rectangle())` on tappable rows
- [ ] `@FocusState` manages keyboard focus in forms
- [ ] Stack/grid `spacing:` omitted unless a specific value is required
## References
- Grid patterns: [references/grids.md](references/grids.md)
- List and section patterns: [references/list.md](references/list.md)
- ScrollView and lazy stacks: [references/scrollview.md](references/scrollview.md)
- Form patterns: [references/form.md](references/form.md)
- Architecture and state management: see `swiftui-patterns` skill
- Navigation patterns: see `swiftui-navigation` skill
Creator's repository · dpearson2699/swift-ios-skills