# Grids

Grid guidance for the `swiftui-layout-components` skill.

## When a grid is the right shape

Reach for a grid when items are visual and a one-per-row list would waste
width or slow scanning: icon pickers, photo and video galleries, template
choosers, settings tiles, dashboards of cards. On iOS the everyday choice is a
`LazyVGrid` in a vertical `ScrollView`. `Grid` is for a handful of items that
do not scroll, or when you need to compose rows explicitly.

## Picking the grid type

| Type | Pick it when |
|------|--------------|
| `LazyVGrid` | Many items, vertical scrolling, column count should follow width. The usual iPhone and iPad case, and the safe default for big data sets |
| `LazyHGrid` | Horizontal scrolling is the main motion and rows are easier to describe than columns |
| `Grid` + `GridRow` | Few, mostly static items where alignment across rows and columns matters more than laziness |

## Column strategies

**Adaptive.** `GridItem(.adaptive(minimum: 110, maximum: 220))` fits as many
columns as the width allows. Start here for photo grids, template choosers and
icon pickers.

**Flexible.** Two or three `.flexible(minimum: 100)` items give a steady
two-up or three-up rhythm regardless of device.

**Fixed.** `.fixed(width)` only when an exact width is required. It copes
poorly with different screen sizes and with Dynamic Type.

## Sizing cells

A cell decides its own shape; it never asks the parent how big it is.

- Square tiles come from `.aspectRatio(1, contentMode: .fit)`.
- `.frame(maxWidth: .infinity)` to fill the column.
- Padding inside the cell instead of arithmetic on geometry.

Avoid `GeometryReader` in lazy containers: it breaks lazy layout and adds
measurement passes. If a cell truly must react to its size, use
`.onGeometryChange(for:of:action:)`. Apple lists it for iOS 16.0+, macOS
13.0+, tvOS 16.0+, watchOS 9.0+ and visionOS 1.0+ (back-deployed, so compile
with the Xcode 16 SDK or newer).

## Adaptive picker with selection

```swift
struct WallpaperPicker: View {
    let wallpapers: [Wallpaper]
    @Binding var chosen: Wallpaper.ID?

    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(.adaptive(minimum: 104, maximum: 180))]) {
                ForEach(wallpapers) { paper in
                    Button { chosen = paper.id } label: {
                        ZStack(alignment: .bottomTrailing) {
                            Image(paper.assetName)
                                .resizable()
                                .aspectRatio(1, contentMode: .fit)
                                .clipShape(.rect(cornerRadius: 10))
                            if chosen == paper.id {
                                Image(systemName: "checkmark.circle.fill")
                                    .foregroundStyle(.white, .tint)
                                    .padding(6)
                            }
                        }
                    }
                    .buttonStyle(.plain)
                    .contentShape(Rectangle())
                    .accessibilityLabel(paper.title)
                    .accessibilityAddTraits(chosen == paper.id ? .isSelected : [])
                }
            }
            .padding()
        }
    }
}
```

Adaptive columns follow the size class, the image's aspect ratio (not geometry)
makes the tile square, and the whole tile is the hit area.

## Three-up media gallery

```swift
struct ClipGallery: View {
    let clips: [Clip]
    private let columns = [GridItem](
        repeating: GridItem(.flexible(minimum: 96), spacing: 3),
        count: 3
    )

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 3) {
                ForEach(clips) { clip in
                    ClipThumbnail(clip: clip)
                        .aspectRatio(1, contentMode: .fill)
                        .clipShape(.rect(cornerRadius: 4))
                }
            }
        }
    }
}
```

Use this when every row should show exactly three thumbnails.

## Sections in a grid

```swift
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
    ForEach(shelves) { shelf in
        Section {
            ForEach(shelf.books) { BookCover(book: $0) }
        } header: {
            Text(shelf.name)
                .font(.headline)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}
```

Grouping works well for categories or a recents row. Keep headers small; big
headers slow down scanning.

## Selection and interaction

- The whole cell is the tap target (`.contentShape(Rectangle())`).
- Mark selection the same way everywhere: one corner badge or one border style.
- Pressed, hover and focus states should be quiet but visible.
- Few overlays per cell.
- Multi-select needs a visible affordance; do not hide it behind long press.

## Performance

- `LazyVGrid` for large collections.
- Downsample big images before display.
- No expensive formatting or filtering inside cell bodies; precompute grouped
  or filtered arrays.
- Stable identity for every item.
- Janky scrolling? Measure first: image decoding, overlays on each cell and
  state that invalidates too often are the usual causes, and fixing them is
  cheaper than rebuilding the grid.

## Accessibility

- Group clearly and space consistently.
- Try the largest Dynamic Type sizes, even for image-heavy cells.
- VoiceOver labels name the item and say whether it is selected.
- On iPad and with a keyboard, focus should move in a sensible order.
- Keep hit targets comfortably large.

## Pitfalls

- A heavy overlay stack on every cell.
- Grids nested inside grids without a reason.
- Filtering inline in `ForEach`.
- Fixed columns where adaptive ones fit the goal.
- `GeometryReader` in `LazyVGrid`, `LazyHGrid`, `LazyVStack` or `LazyHStack`;
  use `.aspectRatio`, or `.onGeometryChange` (iOS 16.0+) when a size must be read.
