Skip to content

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 GridItem with .flexible() sizing
  • How to use Section with pinnedViews: [.sectionHeaders] to create sticky headers
  • The difference between LazyVGrid (lazy, performant) and Grid (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

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

  1. let columns: [GridItem] — Declared as a stored property, not inside body, so it's only computed once. Three .flexible() items means three equal-width columns. The spacing: 6 on each GridItem is the gap between that column and the next one to its right.

  2. LazyVGrid(columns:alignment:spacing:pinnedViews:content:) — The full initializer form is used here to make all parameters explicit. alignment: .center aligns items within each grid cell horizontally. spacing: 6 is the row gap. pinnedViews: [.sectionHeaders] enables sticky headers.

  3. Section(header: ...) inside the gridLazyVGrid supports Section views with header: and footer: content. The header view spans the full width — note .frame(maxWidth: .infinity, alignment: .leading) on the Text to stretch the background color edge-to-edge.

  4. ForEach(0..<20) — The range produces 20 cells per section. In a real app, you'd use ForEach(items, id: \.id) with your actual data model. LazyVGrid only creates cells as they scroll into view, so 40 cells here is no performance problem.

  5. Rectangle().frame(height: 150) — Each cell is a Rectangle with 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 better
LazyVGrid 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 GridItem
LazyVGrid'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 ScrollView
LazyVGrid 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] in LazyVGrid creates sticky section headers as you scroll — requires Section(header:) inside the grid
  • LazyVGrid renders 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

Released under the MIT License.