SwiftUI LazyVGrid and LazyHGrid Explained with Examples

SwiftUI grids make it much easier to display repeated content in rows and columns, especially when you need something more structured than a simple stack. In this article, you will learn what LazyVGrid and LazyHGrid are, how they differ, how to configure their GridItem definitions, and how to use them to build practical layouts such as photo galleries, dashboards, and product lists.

Quick answer: LazyVGrid arranges items in vertical scrolling grid rows, while LazyHGrid arranges items in horizontal scrolling grid columns. Both are called “lazy” because SwiftUI creates child views as needed, which helps performance for larger collections.

Difficulty: Beginner

Helpful to know first: You will understand this better if you already know basic SwiftUI view layout, how ScrollView works, and how to display repeated data with ForEach.

1. What Is LazyVGrid and LazyHGrid?

LazyVGrid and LazyHGrid are SwiftUI container views for displaying content in a grid layout.

The word “lazy” matters because a grid often displays many items. Without lazy loading, SwiftUI would build every child view at once, which can waste memory and hurt performance.

These views are commonly compared with VStack, HStack, and List. Stacks place views in one direction only, while grids let you arrange items across multiple rows and columns. A List is useful for standard scrolling lists, but grids are better when visual alignment and multiple columns matter.

How SwiftUI grid directions differ
LazyVGrid
Columns in vertical scroll
LazyHGrid
Rows in horizontal scroll

Choose the grid direction based on the scroll direction and how you want content arranged.

2. Why LazyVGrid and LazyHGrid Matter

Many apps need more than a simple column of views. A photo app may show image thumbnails, a shopping app may show product cards, and a settings dashboard may show feature tiles. A grid helps present this information clearly and efficiently.

These containers matter because they solve several common layout problems:

You should use a lazy grid when the content is repeated and naturally belongs in cells. You should not use one when the interface only needs a handful of custom-positioned views or when a normal stack is simpler and clearer.

3. Basic Syntax or Core Idea

The core idea is simple: create an array of GridItem values, then pass that array into either LazyVGrid or LazyHGrid.

Defining grid items

A GridItem describes the size behavior of a row or column. The most common options are .fixed, .flexible, and .adaptive.

let columns = [
    GridItem(.fixed(100)),
    GridItem(.flexible()),
    GridItem(.flexible())
]

This creates three columns. The first is always 100 points wide, while the others expand to use the remaining space.

Minimal LazyVGrid example

This example shows a vertically scrolling grid with three flexible columns.

import SwiftUI

struct ContentView: View {
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 12) {
                ForEach(1...12, id: \.self) { item in
                    Text("Item \(item)")
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue.opacity(0.2))
                        .cornerRadius(8)
                }
            }
            .padding()
        }
    }
}

The ScrollView provides vertical scrolling, and the grid arranges the repeated items into three equal columns.

Minimal LazyHGrid example

This version uses rows instead of columns and scrolls horizontally.

import SwiftUI

struct ContentView: View {
    let rows = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: rows, spacing: 12) {
                ForEach(1...12, id: \.self) { item in
                    Text("Item \(item)")
                        .frame(width: 100, height: 80)
                        .background(Color.green.opacity(0.2))
                        .cornerRadius(8)
                }
            }
            .padding()
        }
    }
}

Here, the content moves left to right because the surrounding ScrollView is horizontal.

4. Step-by-Step Examples

Example 1: A simple three-column vertical grid

This is a common starting point for dashboards and galleries.

import SwiftUI

struct NumberGridView: View {
    let columns = Array(repeating: GridItem(.flexible()), count: 3)

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 10) {
                ForEach(1...15, id: \.self) { number in
                    Text("\(number)")
                        .frame(maxWidth: .infinity, minHeight: 70)
                        .background(Color.orange.opacity(0.25))
                        .cornerRadius(10)
                }
            }
            .padding()
        }
    }
}

Each cell expands to fill the available width in its column, so the grid stays evenly spaced.

Example 2: Adaptive columns for different screen sizes

Adaptive grids are especially useful on iPhone and iPad because the number of columns can change automatically based on available width.

import SwiftUI

struct AdaptiveGridView: View {
    let columns = [
        GridItem(.adaptive(minimum: 120, maximum: 180), spacing: 12)
    ]

    let items = Array(1...20)

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 12) {
                ForEach(items, id: \.self) { item in
                    VStack {
                        Image(systemName: "star.fill")
                            .font(.largeTitle)
                            .foregroundColor(.yellow)
                        Text("Card \(item)")
                    }
                    .frame(maxWidth: .infinity, minHeight: 120)
                    .padding()
                    .background(Color.blue.opacity(0.15))
                    .cornerRadius(12)
                }
            }
            .padding()
        }
    }
}

With .adaptive, SwiftUI fits as many columns as it can while respecting the minimum and maximum sizes.

Example 3: Horizontal grid with two rows

This pattern works well for horizontally scrolling categories, templates, or quick actions.

import SwiftUI

struct HorizontalGridView: View {
    let rows = [
        GridItem(.fixed(90)),
        GridItem(.fixed(90))
    ]

    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: rows, spacing: 16) {
                ForEach(1...10, id: \.self) { item in
                    Text("Option \(item)")
                        .frame(width: 120, height: 80)
                        .background(Color.purple.opacity(0.2))
                        .cornerRadius(10)
                }
            }
            .padding()
        }
    }
}

The fixed rows make every tile the same height, which creates a cleaner horizontal layout.

Example 4: Using a model with ForEach

In real apps, grid items usually come from data models rather than hard-coded number ranges.

import SwiftUI

struct Product: Identifiable {
    let id = UUID()
    let name: String
    let price: Double
}

struct ProductGridView: View {
    let columns = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]

    let products = [
        Product(name: "Keyboard", price: 99.0),
        Product(name: "Mouse", price: 49.0),
        Product(name: "Monitor", price: 299.0),
        Product(name: "Dock", price: 129.0)
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(products) { product in
                    VStack(alignment: .leading, spacing: 8) {
                        Rectangle()
                            .fill(Color.gray.opacity(0.2))
                            .frame(height: 100)
                            .overlay(
                                Image(systemName: "shippingbox")
                                    .font(.largeTitle)
                                    .foregroundColor(.gray)
                            )

                        Text(product.name)
                            .font(.headline)
                        Text("$\(product.price, specifier: \"%.2f\")")
                            .foregroundColor(.secondary)
                    }
                    .padding()
                    .background(Color.white)
                    .cornerRadius(12)
                    .shadow(radius: 2)
                }
            }
            .padding()
            .background(Color(.systemGroupedBackground))
        }
    }
}

This example shows the typical production pattern: a grid layout plus model-driven content.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Using a grid without a ScrollView for larger content

Beginners often expect a lazy grid to scroll by itself. The grid container does not provide scrolling automatically.

Problem: Without a surrounding ScrollView, content that does not fit may be clipped or inaccessible, which makes the grid look broken.

let columns = [GridItem(.flexible()), GridItem(.flexible())]

LazyVGrid(columns: columns) {
    ForEach(1...50, id: \.self) { item in
        Text("Item \(item)")
    }
}

Fix: Wrap the grid in a ScrollView that matches the intended direction.

let columns = [GridItem(.flexible()), GridItem(.flexible())]

ScrollView {
    LazyVGrid(columns: columns) {
        ForEach(1...50, id: \.self) { item in
            Text("Item \(item)")
        }
    }
}

The corrected version works because the scroll view provides the scrolling behavior that the grid itself does not own.

Mistake 2: Using unstable or duplicate IDs in ForEach

ForEach needs stable identity so SwiftUI can track view updates correctly.

Problem: If IDs are duplicated or not stable, updates may animate incorrectly, reuse the wrong view, or produce confusing rendering behavior.

struct Tag {
    let name: String
}

let tags = [
    Tag(name: "Swift"),
    Tag(name: "Swift")
]

ForEach(tags, id: \.name) { tag in
    Text(tag.name)
}

Fix: Use a model with a unique identifier, such as UUID or a stable database ID.

struct Tag: Identifiable {
    let id = UUID()
    let name: String
}

let tags = [
    Tag(name: "Swift"),
    Tag(name: "Swift")
]

ForEach(tags) { tag in
    Text(tag.name)
}

The corrected version works because each grid cell has a unique identity that SwiftUI can track safely.

Mistake 3: Choosing the wrong GridItem size type

Many layout problems happen because the column or row strategy does not match the content.

Problem: Using only fixed sizes on small screens can cause cramped layouts, while using flexible sizes for content that needs a consistent width can produce uneven designs.

let columns = [
    GridItem(.fixed(200)),
    GridItem(.fixed(200))
]

Fix: Use .flexible or .adaptive when the layout should respond to different device widths.

let columns = [
    GridItem(.adaptive(minimum: 120, maximum: 180))
]

The corrected version works because the grid can now adapt its column count and width to the available space.

Mistake 4: Expecting LazyVGrid and LazyHGrid to behave like List

A grid is not a drop-in replacement for every list screen. List includes built-in row behaviors and styling that a grid does not.

Problem: If you expect automatic separators, swipe actions, or default list styling from a grid, the interface may feel incomplete or inconsistent.

ScrollView {
    LazyVGrid(columns: [GridItem(.flexible())]) {
        ForEach(1...5, id: \.self) { item in
            Text("Row \(item)")
        }
    }
}

Fix: Use a grid when you need a true grid layout, and use List when you need list-specific interaction and presentation.

List(1...5, id: \.self) { item in
    Text("Row \(item)")
}

The corrected version works because List is built for list-style interfaces, while lazy grids are built for multi-column or multi-row layouts.

7. Best Practices

Use adaptive columns when the layout should scale across devices

Hard-coding a fixed number of columns can make a grid look too cramped on iPhone or too sparse on iPad. Adaptive sizing often gives a better result.

let columns = [
    GridItem(.adaptive(minimum: 140), spacing: 12)
]

This approach lets the layout respond naturally to screen width changes.

Keep cell views reusable and small

If each grid item contains a lot of layout code, your view becomes harder to read and maintain. Extracting a cell into its own view is often cleaner.

struct ProductCard: View {
    let title: String

    var body: some View {
        Text(title)
            .frame(maxWidth: .infinity, minHeight: 100)
            .background(Color.blue.opacity(0.15))
            .cornerRadius(10)
    }
}

A reusable cell keeps the grid code focused on layout instead of mixing layout and presentation logic together.

Match grid direction to content flow

Use LazyVGrid when users naturally expect to scroll up and down through many items. Use LazyHGrid for short horizontal browsing sections.

// Vertical browsing through many items
ScrollView {
    LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())]) {
    }
}

// Horizontal browsing through featured items
ScrollView(.horizontal) {
    LazyHGrid(rows: [GridItem(.fixed(80))]) {
    }
}

Choosing the correct direction improves usability because the scrolling behavior matches user expectations.

8. Limitations and Edge Cases

9. Practical Mini Project

Let’s build a small photo-style gallery screen with adaptive columns. This example is simple, but it uses the same ideas you would apply in a real app.

import SwiftUI

struct GalleryItem: Identifiable {
    let id = UUID()
    let title: String
    let symbol: String
}

struct GalleryView: View {
    let columns = [
        GridItem(.adaptive(minimum: 120), spacing: 16)
    ]

    let items = [
        GalleryItem(title: "Mountains", symbol: "mountain.2.fill"),
        GalleryItem(title: "Sunset", symbol: "sunset.fill"),
        GalleryItem(title: "Forest", symbol: "leaf.fill"),
        GalleryItem(title: "Ocean", symbol: "water.waves"),
        GalleryItem(title: "Night", symbol: "moon.stars.fill"),
        GalleryItem(title: "City", symbol: "building.2.fill")
    ]

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 16) {
                ForEach(items) { item in
                    VStack(spacing: 12) {
                        ZStack {
                            RoundedRectangle(cornerRadius: 12)
                                .fill(Color.blue.opacity(0.15))
                                .frame(height: 110)

                            Image(systemName: item.symbol)
                                .font(.system(size: 36))
                                .foregroundColor(.blue)
                        }

                        Text(item.title)
                            .font(.headline)
                    }
                    .frame(maxWidth: .infinity)
                    .padding(8)
                    .background(Color(.secondarySystemBackground))
                    .cornerRadius(14)
                }
            }
            .padding()
        }
        .navigationTitle("Gallery")
    }
}

This mini project uses one adaptive GridItem, so SwiftUI decides how many columns can fit. Each gallery card expands to fill its column, which keeps the layout balanced.

You can grow this example by adding selection, navigation, remote images, or filtering. The core grid structure will remain the same.

10. Key Points

11. Practice Exercise

Build a simple SwiftUI grid that displays 12 colored cards.

Expected output: A vertically scrolling two-column grid of 12 cards.

Hint: Create an array with Array(repeating: GridItem(.flexible()), count: 2) and use ForEach(1...12, id: \.self).

import SwiftUI

struct PracticeGridView: View {
    let columns = Array(repeating: GridItem(.flexible()), count: 2)

    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 12) {
                ForEach(1...12, id: \.self) { item in
                    Text("Card \(item)")
                        .frame(maxWidth: .infinity, minHeight: 100)
                        .background(Color.teal.opacity(0.25))
                        .cornerRadius(12)
                }
            }
            .padding()
        }
    }
}

12. Final Summary

LazyVGrid and LazyHGrid are the main SwiftUI tools for showing repeated content in structured grids. The key difference is direction: LazyVGrid works best for vertical scrolling layouts with columns, while LazyHGrid works best for horizontal scrolling layouts with rows.

You also saw that most grid behavior comes from the GridItem array. Once you understand .fixed, .flexible, and .adaptive, you can build layouts that are predictable, reusable, and responsive across device sizes. A good next step is to practice extracting reusable grid cell views and combining grids with real app data models.