All skills
dpearson2699 avatar

/swiftui-layout-components

@45c9085

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 and scroll-driven reveal surfaces, Form with validation, Toggle/Picker/Slider, .searchable, and overlay patterns. Use when building data-driven layouts, collection views, paged detail reveals, settings screens, search interfaces, or transient overlay UI.

Use this Skill: https://skilld.dev/gh/dpearson2699/swift-ios-skills/swiftui-layout-components

This session only. Nothing lands on disk.

referencesgrids.md

≈1.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Grids

Contents

  • Intent
  • Choosing the right grid type
  • Column strategies
  • Cell sizing and aspect ratio
  • Example: adaptive icon grid
  • Example: fixed 3-column media grid
  • Example: sectioned grid
  • Selection and interaction patterns
  • Performance guardrails
  • Accessibility and polish
  • Pitfalls

Intent

Use grids for dense visual collections where row-based layouts waste space or make scanning harder.

Good fits:

  • icon pickers
  • media galleries
  • template choosers
  • settings tiles
  • dashboards with repeatable cards

Default to LazyVGrid for vertically scrolling collections on iOS. Reach for Grid when content is small and non-scrollable, or when you need explicit row composition rather than a large lazy container.

Choosing the right grid type

LazyVGrid

Use for the common iPhone/iPad case: many items, vertical scrolling, and a column definition that should adapt to width.

LazyHGrid

Use when horizontal scrolling is the dominant interaction and rows are easier to define than columns.

Grid

Use when:

  • the item count is small
  • the content is mostly static
  • row/column relationships matter more than lazy loading
  • you need GridRow composition instead of a repeated collection layout

For large scrolling datasets, LazyVGrid is usually the safer default.

Column strategies

Adaptive columns

Use .adaptive when you want the number of columns to respond to available width.

let columns = [GridItem(.adaptive(minimum: 120, maximum: 240))]

This is the best default for icon pickers, template choosers, and photo grids that should scale naturally across iPhone and iPad sizes.

Flexible columns

Use multiple .flexible columns when you want a predictable column count.

let columns = [
    GridItem(.flexible(minimum: 100)),
    GridItem(.flexible(minimum: 100)),
    GridItem(.flexible(minimum: 100)),
]

This works well when design requires a fixed 2-column or 3-column rhythm.

Fixed columns

Use .fixed only when the design truly requires exact widths. Fixed columns are less resilient across device sizes and dynamic type changes.

Cell sizing and aspect ratio

Grid cells should usually define their own shape without reading parent geometry.

Preferred sizing tools:

  • .aspectRatio(1, contentMode: .fit) for square thumbnails
  • .frame(maxWidth: .infinity) when content should fill the available column
  • internal padding instead of outer geometry math

Avoid GeometryReader inside lazy containers. It defeats lazy layout and adds extra measurement work.

Example: adaptive icon grid

let columns = [GridItem(.adaptive(minimum: 120, maximum: 240))]

LazyVGrid(columns: columns) {
    ForEach(icons) { icon in
        Button {
            select(icon)
        } label: {
            ZStack(alignment: .bottomTrailing) {
                Image(icon.previewName)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .clipShape(.rect(cornerRadius: 6))

                if icon.isSelected {
                    Image(systemName: "checkmark.seal.fill")
                        .padding(4)
                        .tint(.green)
                }
            }
        }
        .buttonStyle(.plain)
        .contentShape(Rectangle())
    }
}

Why this works:

  • adaptive columns scale across width classes
  • square-ish cells come from image aspect ratio, not geometry reads
  • the whole tile remains tappable

Example: fixed 3-column media grid

LazyVGrid(
    columns: [
        .init(.flexible(minimum: 100), spacing: 4),
        .init(.flexible(minimum: 100), spacing: 4),
        .init(.flexible(minimum: 100), spacing: 4),
    ],
    spacing: 4
) {
    ForEach(items) { item in
        ThumbnailView(item: item)
            .aspectRatio(1, contentMode: .fit)
            .clipShape(.rect(cornerRadius: 8))
    }
}

Use this when the design language wants a consistent three-up gallery instead of an adaptive count.

Example: sectioned grid

Sectioned grids work well for grouped content like categories or recents.

LazyVGrid(columns: [GridItem(.adaptive(minimum: 140))]) {
    ForEach(sections) { section in
        Section {
            ForEach(section.items) { item in
                Tile(item: item)
            }
        } header: {
            Text(section.title)
                .font(.headline)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(.top)
        }
    }
}

Keep headers visually lightweight. Dense grids lose scan efficiency when every section header is oversized or heavily decorated.

Selection and interaction patterns

Grid interactions should stay obvious and forgiving.

Useful defaults:

  • make the entire cell tappable with .contentShape(Rectangle())
  • show selection state in one consistent corner or border treatment
  • keep hover/focus/pressed states subtle but visible
  • avoid stacking too many overlays on every item

For multi-select grids, prefer clear selection affordances instead of hidden state changes triggered only by long press.

Performance guardrails

  • Use LazyVGrid for large collections.
  • Keep overlay count low in every cell.
  • Downsample large images before display.
  • Avoid expensive formatting or filtering in cell bodies.
  • Prefer stable item identity in ForEach.
  • Precompute grouped or filtered collections before rendering.

If scrolling stutters, profile image decoding, per-cell overlays, and repeated state invalidation before changing the grid structure itself.

Accessibility and polish

  • Maintain clear visual grouping and consistent spacing.
  • Test with Dynamic Type even if cells are primarily visual.
  • Ensure VoiceOver labels describe the item and selection state.
  • Use meaningful focus order on iPad and keyboard-driven flows.
  • Avoid tiny hit targets even when cells are visually dense.

A dense grid should still feel calm and legible, not like a wall of competing badges and borders.

Pitfalls

  • Avoid heavy overlays in every grid cell; it can be expensive.
  • Don’t nest grids inside other grids without a clear reason.
  • Don’t put filtering logic inline in ForEach.
  • Don’t use fixed columns where adaptive columns better match the product goal.
  • Never place GeometryReader inside lazy containers (LazyVGrid, LazyHGrid, LazyVStack, LazyHStack). It forces eager measurement and defeats lazy loading. Use .aspectRatio for sizing, or .onGeometryChange if you need to read dimensions. The single-new-value overload is iOS 16+; the old/new-value action overload is iOS 18+.

Source: SKILL.md on GitHub

1 warning17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill is a set of instructional documentation and Swift code patterns for building SwiftUI layouts. It contains no executable scripts, external dependencies, or network operations, and no security risks were identified during the analysis.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: MEDIUM · 1 issue

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 45c9085. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 3 months ago
  • swiftui
  • ios
  • layout
  • vstack
  • hstack
  • zstack
  • lazyvgrid
  • lazyhgrid
  • list
  • form
  • picker
  • toggle
  • slider
  • searchable
  • scrollview

README badge

README badge for dpearson2699/swift-ios-skills/swiftui-layout-components

Provides SwiftUI layout patterns for iOS 17+ apps: VStack/HStack/ZStack, LazyVGrid/LazyHStack, List with sections, ScrollView with ScrollPosition, Form with validation controls, search, and overlay UI. Use when building data-driven layouts, collection views, settings screens, or search interfaces.

Generated from the current SKILL.md.

What iOS versions does this skill cover?
Targets iOS 26+ with Swift 6.3, with patterns backward-compatible to iOS 17 unless explicitly noted (e.g., `.scrollEdgeEffectStyle` is iOS 26 only).
When should I use LazyVStack vs regular VStack?
Use regular VStack for small, fixed content like headers and forms. Use LazyVStack for large or dynamic collections (feeds, chat messages) inside ScrollView to render child views on demand.
Can I use GeometryReader inside lazy containers?
No. Placing GeometryReader inside lazy containers forces eager measurement and defeats lazy loading. Use `.onGeometryChange` (iOS 16+) if you need to read dimensions.
What's the difference between List and ScrollView with LazyVStack?
List provides built-in row reuse, selection, and accessibility for feeds and settings. ScrollView with LazyVStack offers custom layout flexibility for mixed content and horizontal scrolling. Don't nest them in the same hierarchy.
How do I debounce search input?
Use `.task(id: searchQuery)` to trigger async search only when the query changes, and add a small delay (e.g., `Task.sleep(for: .milliseconds(250))`) before fetching to avoid overfetching.

Generated from the current SKILL.md. These answers refresh after source changes.