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.
- 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.
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:
- 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 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
- 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.
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
- 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.
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).
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.