How to use Grid in SwiftUI | SwiftUI Bootcamp #73
LazyVGrid is how you build photo galleries, app store grids, and masonry-style layouts in SwiftUI. By combining flexible GridItem columns with Section headers and pinnedViews, you can create a performant, scrollable grid with sticky section headers — all in declarative code.
What You'll Learn
- How to define grid columns using
GridItemwith.flexible()sizing - How to use
SectionwithpinnedViews: [.sectionHeaders]to create sticky headers - The difference between
LazyVGrid(lazy, performant) andGrid(eager, alignment-aware)
Mental Model
Think of LazyVGrid like a newspaper layout grid. The columns are defined upfront — you decide there are three columns of equal width. The content then flows into those columns row by row, like words flowing into newspaper columns. "Lazy" means items are only created when they scroll into view — just like a newspaper printer only prints pages as they're needed, not the entire paper at once.
The pinnedViews: [.sectionHeaders] feature is like the sticky category tabs in a contacts app — when you scroll into a section, its header stays pinned at the top so you always know which section you're in, even mid-scroll.
Detailed Explanation
LazyVGrid arranges content in vertical grid with columns defined by an array of GridItem values. Each GridItem describes one column's sizing behavior. .flexible() means the column takes an equal share of available width. .fixed(100) would give a column a fixed 100-point width. .adaptive(minimum: 80) creates as many columns as fit, each at least 80 points wide — useful for truly responsive grids.
The spacing parameter on LazyVGrid is the vertical spacing between rows. The spacing on each GridItem is the horizontal spacing between that column and the next. Setting both creates the consistent gutters you see in photo grids.
pinnedViews: [.sectionHeaders] pins the section headers to the top of the scroll view while items from that section are still visible. It requires that your content is wrapped in Section views with header: content provided. This is the SwiftUI equivalent of UICollectionView's sticky supplementary views.
LazyVGrid is lazy — items are not rendered until they're about to enter the visible area. This is critical for large data sets (photo libraries, product catalogs). The non-lazy Grid container (also available from iOS 16) is for small, table-like layouts where you need precise column alignment across rows — it renders all items eagerly.
Code Structure
GridBootcamp.swift defines three equal flexible columns and a LazyVGrid with two Sections. Section 1 contains 20 gray rectangles; Section 2 contains 20 green rectangles. Both section headers use .sectionHeaders pinning, so you can observe the sticky behavior as you scroll. An orange rectangle at the top demonstrates that LazyVGrid can coexist with other views in a ScrollView.
Complete Code
GridBootcamp.swift
import SwiftUI
struct GridBootcamp: View {
// Three equal flexible columns with 6-point horizontal gaps between them
let columns: [GridItem] = [
GridItem(.flexible(), spacing: 6, alignment: nil),
GridItem(.flexible(), spacing: 6, alignment: nil),
GridItem(.flexible(), spacing: 6, alignment: nil),
]
var body: some View {
ScrollView {
Rectangle()
.fill(Color.orange)
.frame(height: 400) // Header banner above the grid
LazyVGrid(
columns: columns,
alignment: .center,
spacing: 6, // Vertical gap between grid rows
pinnedViews: [.sectionHeaders], // Headers stick to the top while their section is visible
content: {
Section(header:
Text("Section 1")
.foregroundColor(.white)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.blue)
.padding()
) {
ForEach(0..<20) { index in
Rectangle()
.frame(height: 150) // Each cell is 150 points tall
}
}
Section(header:
Text("Section 2")
.foregroundColor(.white)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.red)
.padding()
) {
ForEach(0..<20) { index in
Rectangle()
.fill(Color.green)
.frame(height: 150) // Green cells for visual distinction from Section 1
}
}
})
}
}
}
struct GridBootcamp_Previews: PreviewProvider {
static var previews: some View {
GridBootcamp()
}
}Code Walkthrough
let columns: [GridItem]— Declared as a stored property, not insidebody, so it's only computed once. Three.flexible()items means three equal-width columns. Thespacing: 6on eachGridItemis the gap between that column and the next one to its right.LazyVGrid(columns:alignment:spacing:pinnedViews:content:)— The full initializer form is used here to make all parameters explicit.alignment: .centeraligns items within each grid cell horizontally.spacing: 6is the row gap.pinnedViews: [.sectionHeaders]enables sticky headers.Section(header: ...)inside the grid —LazyVGridsupportsSectionviews withheader:andfooter:content. The header view spans the full width — note.frame(maxWidth: .infinity, alignment: .leading)on theTextto stretch the background color edge-to-edge.ForEach(0..<20)— The range produces 20 cells per section. In a real app, you'd useForEach(items, id: \.id)with your actual data model.LazyVGridonly creates cells as they scroll into view, so 40 cells here is no performance problem.Rectangle().frame(height: 150)— Each cell is aRectanglewith a fixed height but no fixed width — the grid column determines the width. This is the correct pattern: let the grid set the width, and you set the height.
Common Mistakes
Mistake: Using LazyVGrid for a small static table where Grid (iOS 16) would be betterLazyVGrid is performance-optimized for long scrollable content but doesn't offer per-column alignment. The static Grid container (available iOS 16+) gives you true table-like column alignment. Use LazyVGrid for scrollable data, Grid for forms and aligned data tables.
Mistake: Setting spacing only on LazyVGrid and forgetting spacing on GridItemLazyVGrid's spacing parameter controls row gaps (vertical). GridItem's spacing controls column gaps (horizontal). To get a consistent gutter on all sides, set both to the same value. Forgetting GridItem.spacing leaves columns flush against each other.
Mistake: Wrapping LazyVGrid in a VStack instead of a ScrollViewLazyVGrid needs a ScrollView parent to scroll. Placing it in a VStack makes it show at its full height, which for large datasets means a massive view that takes forever to render. Always embed LazyVGrid in a ScrollView.
Key Takeaways
- Define columns with an array of
GridItem—.flexible()for equal widths,.fixed(_:)for precise widths,.adaptive(minimum:)for responsive count pinnedViews: [.sectionHeaders]inLazyVGridcreates sticky section headers as you scroll — requiresSection(header:)inside the gridLazyVGridrenders items lazily (as they scroll into view) — it's the right choice for large, scrollable datasets like image galleries or product feeds
Last updated: June 27, 2026