# SwiftUI LazyVGrid and LazyHGrid Explained with Examples

Source: https://devdocs10.com/articles/swift/swift-grids  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

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.

- `LazyVGrid` lays out items in columns and usually scrolls vertically.
- `LazyHGrid` lays out items in rows and usually scrolls horizontally.
- Both use an array of `GridItem` values to define the grid structure.
- Both are lazy, which means SwiftUI does not create every child view immediately.
- They are ideal when you want repeated content in a neat grid instead of a single vertical or horizontal stack.

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.

## 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:

- They let you display repeated data in a more compact layout than a list.
- They make adaptive layouts possible, such as automatically fitting more columns on wider screens.
- They work well with large data sets because of lazy view creation.
- They separate layout rules from content by using `GridItem` definitions.

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

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`.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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

- Photo galleries where thumbnails should line up evenly across the screen.
- Shopping screens with product cards that include image, title, and price.
- Dashboard screens with tiles for actions, stats, or categories.
- Template pickers, emoji pickers, or icon selectors with many repeated choices.
- Horizontally scrolling sections such as “recent files” or “recommended items” using `LazyHGrid`.
- Adaptive iPhone and iPad layouts where the number of columns changes with available space.

## 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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

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

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

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
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.

```swift
// 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

- `LazyVGrid` and `LazyHGrid` are available in modern SwiftUI, so older deployment targets may limit usage depending on your app’s platform support.
- The grid itself does not scroll; scrolling comes from the surrounding `ScrollView`.
- Very complex cell views can still affect performance even though the container is lazy.
- Adaptive sizing can sometimes produce more or fewer columns than a beginner expects, especially when padding and spacing reduce available width.
- If your cells do not expand with `maxWidth: .infinity` where appropriate, the grid may look uneven because content sizes differ.
- `LazyHGrid` can feel awkward for long data sets if users expect vertical scrolling, so direction choice matters.
- If a grid “is not scrolling,” the most common cause is missing or incorrectly oriented `ScrollView`.

## 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.

```swift
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

- `LazyVGrid` is for vertically scrolling grids arranged by columns.
- `LazyHGrid` is for horizontally scrolling grids arranged by rows.
- Both rely on `GridItem` definitions to control size and spacing.
- `.fixed`, `.flexible`, and `.adaptive` each solve different layout needs.
- Wrap lazy grids in a matching `ScrollView` when content can exceed the available space.
- Use stable IDs in `ForEach` so updates behave correctly.
- Adaptive grids are often the best choice for layouts that must work across multiple device sizes.

## 11. Practice Exercise

Build a simple SwiftUI grid that displays 12 colored cards.

- Use `LazyVGrid`.
- Create two flexible columns.
- Show the text `Card 1` through `Card 12`.
- Wrap the grid in a vertical `ScrollView`.
- Give each card padding, a background color, and rounded corners.

**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)`.

```swift
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.
