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
Gridcontainer arranges children into rows and columns - How
gridCellColumns,gridCellAnchor, andgridColumnAlignmentfine-tune cell behavior - The difference between
Grid(eager, built for data-driven or complex layouts) andLazyVGrid/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
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
Grid(alignment: .center, horizontalSpacing: 8, verticalSpacing: 8)— Creates the grid container.alignmentis the fallback for cells that don't specify their own alignment.horizontalSpacingandverticalSpacingset the gutters between cells.ForEach(0..<4) { rowIndex in— Generates 4 rows. Combined withForEach(0..<4)for columns, this creates a 4×4 = 16 cell grid.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.EmptyView()for cell 7 — Inserts an invisible placeholder in the cell 7 position, leaving it visually empty while maintaining the grid's column structure..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..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..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.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+GridRowfor structured, fixed-size layouts (dashboards, forms); useLazyVGridfor large scrollable collections (galleries) gridCellColumns(N)merges N columns;EmptyView()skips a cell;gridColumnAlignment()aligns an entire columnGridis eager (creates all cells at render time) — for large datasets, switch toLazyVGridinside aScrollView
Last updated: June 27, 2026