Skip to content

LazyVGrid, LazyHGrid, and GridItems in SwiftUI | SwiftUI Bootcamp #16

The Grid view (and its lazy predecessors LazyVGrid/LazyHGrid) give you precise control over multi-column and multi-row layouts — perfect for photo galleries, app icon grids, dashboards, and spreadsheet-style screens. After this lesson you'll understand how GridRow, gridCellColumns, and alignment modifiers combine to produce complex, responsive grid layouts.

What You'll Learn

  • How the SwiftUI Grid container arranges children into rows and columns
  • How gridCellColumns, gridCellAnchor, and gridColumnAlignment fine-tune cell behavior
  • The difference between Grid (eager, built for data-driven or complex layouts) and LazyVGrid/LazyHGrid (lazy, for large scrollable datasets)

Mental Model

Think of a Grid like a spreadsheet. GridRow is a row of cells. Each cell automatically gets equal column width by default, like a spreadsheet with uniform column widths. gridCellColumns(2) is like merging two cells in a row — that cell now spans two columns. gridColumnAlignment sets the text alignment inside a specific column, just as you'd right-align numbers in a column of a spreadsheet.

The difference between Grid and LazyVGrid: Grid is like printing the entire spreadsheet at once — great for known, finite data. LazyVGrid is like loading a spreadsheet one screen at a time — great for large datasets where you don't want to build all cells upfront.

Detailed Explanation

SwiftUI has two grid systems. The Grid container (introduced iOS 16) is eager — it sizes all rows and columns together to achieve alignment across rows. Use it for dashboards, forms, and structured data where you know the cell count. LazyVGrid/LazyHGrid (iOS 14+) are lazy — they only create cells near the scroll viewport. Use them for photo galleries and large collections.

Grid(alignment:horizontalSpacing:verticalSpacing:) is the container. Its children are GridRow instances. Items inside a GridRow are distributed into columns automatically.

gridCellColumns(N) makes a cell span N columns — essential for "featured" items or section headers inside a grid. gridCellAnchor(.trailing) positions the cell's content at the trailing edge within its allocated cell space. gridColumnAlignment(.leading) sets the alignment for an entire column — useful for making all cells in column 1 right-aligned regardless of content size.

EmptyView() can be used as a placeholder to skip a cell in a grid row — the column still exists but renders nothing.

Code Structure

The sample is in GridViewBootcamp.swift and uses a 4×4 Grid with special handling for cell 6 (spans 2 columns) and cell 7 (skipped with EmptyView). Each cell is produced by a private cell(int:) helper. The commented-out code at the bottom shows a simpler 2-row Grid with a Divider.

Complete Code

GridViewBootcamp.swift

swift
import SwiftUI

struct GridViewBootcamp: View {
    var body: some View {
        Grid(alignment: .center, horizontalSpacing: 8, verticalSpacing: 8) { // 8pt gaps between all cells
            ForEach(0..<4) { rowIndex in
                GridRow(alignment: .bottom) { // aligns all cells in this row to their bottom edge
                    ForEach(0..<4) { columnIndex in
                        let cellNumber = (rowIndex * 4) + (columnIndex + 1) // converts 2D index to sequential cell number
                        
                        if cellNumber == 7 {
                            EmptyView() // skips cell 7, leaving that grid position empty
//                            Color.green
//                                .gridCellUnsizedAxes([.horizontal, .vertical]) // fills space without contributing to column sizing
                        } else {
                            cell(int: cellNumber)
                                .gridCellColumns(cellNumber == 6 ? 2 : 1) // cell 6 spans 2 columns to compensate for skipped cell 7
                                .gridCellAnchor(.trailing)  // anchors each cell's content to its trailing edge
                                .gridColumnAlignment(
                                    cellNumber == 1 ? .trailing : // column 1 is right-aligned
                                    cellNumber == 4 ? .leading :  // column 4 (last column, first row) is left-aligned
                                    .center                        // all other columns center their content
                                )
                        }
                    }
                }
            }
        }
        
//        Grid() {
//            GridRow {
//                cell(int: 1)
//                cell(int: 2)
//                cell(int: 3)
//            }
//            
//            Divider()
//                .gridCellUnsizedAxes(.horizontal) // lets the divider span full width without affecting column widths
////            cell(int: 33333333333333)
//            
//            GridRow {
//                cell(int: 4)
//                cell(int: 5)
//            }
//        }
    }
    
    private func cell(int: Int) -> some View { // extracted helper to keep the grid body readable
        Text("\(int)")
            .frame(height: int == 10 ? 20 : nil) // cell 10 gets a shorter height for demonstrating row height variation
            .font(.largeTitle)
            .padding()
            .background(Color.blue) // blue background makes each cell visible and distinct
    }
}

#Preview {
    GridViewBootcamp()
}

Code Walkthrough

  1. Grid(alignment: .center, horizontalSpacing: 8, verticalSpacing: 8) — Creates the grid container. alignment is the fallback for cells that don't specify their own alignment. horizontalSpacing and verticalSpacing set the gutters between cells.
  2. ForEach(0..<4) { rowIndex in — Generates 4 rows. Combined with ForEach(0..<4) for columns, this creates a 4×4 = 16 cell grid.
  3. let cellNumber = (rowIndex * 4) + (columnIndex + 1) — Maps the 2D (rowIndex, columnIndex) pair to sequential numbers 1–16. Row 0 = cells 1–4, row 1 = cells 5–8, etc.
  4. EmptyView() for cell 7 — Inserts an invisible placeholder in the cell 7 position, leaving it visually empty while maintaining the grid's column structure.
  5. .gridCellColumns(cellNumber == 6 ? 2 : 1) — Cell 6 spans 2 columns, effectively merging cell 6 and 7's positions. This compensates for the empty cell 7 and keeps the row looking intentional rather than broken.
  6. .gridCellAnchor(.trailing) — Positions the cell content (the blue rectangle) at the trailing edge within the cell's allocated space. This only has a visible effect when the cell is wider than its content.
  7. .gridColumnAlignment(...) — This modifier is evaluated once per cell but applies to the entire column in the grid. Setting it on cell 1 (column 1) makes the whole first column right-aligned; setting it on cell 4 makes the whole last column left-aligned.
  8. private func cell(int:) -> some View — Extracts repeated view logic into a helper function. This is a standard pattern for complex grids where each cell uses the same structure.

Common Mistakes

Mistake: Using Grid for a large photo gallery and seeing poor scroll performanceGrid is eager — it creates all cells immediately. For galleries with 100+ images, use LazyVGrid inside a ScrollView instead. LazyVGrid only creates cells near the visible viewport, dramatically reducing memory and render time.

Mistake: Applying gridCellColumns and wondering why the row still looks misalignedgridCellColumns(2) makes a cell span 2 columns, but the columns it spans are determined by its position in the GridRow. If cell 6 spans 2 columns and cell 7 is empty, they together fill the last 2 column slots of the row correctly. But if you miscalculate which column a cell lands in, the span may overshoot the row. Always verify by temporarily adding colored backgrounds.

Mistake: Using Divider() directly in a Grid without .gridCellUnsizedAxes(.horizontal) A bare Divider inside a Grid takes up one cell's width by default, not the full grid width. Add .gridCellUnsizedAxes(.horizontal) to let it span the full width without influencing individual column width calculations.

Key Takeaways

  • Use Grid + GridRow for structured, fixed-size layouts (dashboards, forms); use LazyVGrid for large scrollable collections (galleries)
  • gridCellColumns(N) merges N columns; EmptyView() skips a cell; gridColumnAlignment() aligns an entire column
  • Grid is eager (creates all cells at render time) — for large datasets, switch to LazyVGrid inside a ScrollView

Last updated: June 27, 2026

Released under the MIT License.