SwiftUI Field Guide

LazyVGrid

In SwiftUI, there are multiple options for displaying a grid of items. Lazy grids have been available since iOS 14, and the Grid view is available from iOS 16 and up.

On this page, we will focus on the layout behavior of a lazy grid view. The grid is named “lazy” because it only creates the views when needed (similar to a collection view or a table view in UIKit). In order for the grid to actually be lazy, we need to wrap it in a scroll view. Because the grid avoids creating all the children upfront, its layout behavior is different from most views. Specifically, the widths of the grid columns are computed without consulting the subviews, but are based purely on the column definitions.

Below is a simple example of a grid with two columns. The first column is flexible with a minimum width of 40 points, and the second column is fixed at 60 points. This works as expected, and we'll discuss the actual algorithm in detail later on this page. In the example below, we can see that the second column always becomes 60 points wide, and the first column stretches to fill the remaining space, taking its minimum width into account.

Code
ScrollView {    LazyVGrid(columns: [                  .init(.flexible(minimum: 40)),                  .init(.fixed(60))              ]) {        ColoredBoxes()    }    .padding(8)}
Preview
200

In lazy grids, we can choose whether you want a fixed number of columns or a flexible number of columns. When we're displaying something similar to tabular data, we would choose a fixed number of columns, whereas a photo grid often would have a flexible number of columns depending on the available width. To create a flexible number of columns, we can use the .adaptive column size, which tries to fit as many columns as possible inside the available width:

Code
ScrollView {    LazyVGrid(columns: [                  .init(.adaptive(                            minimum: 
), spacing: 2) ], spacing: 2) { PhotoPlaceholders() }}
Preview
300

Here is a similar example, but with cells that fill the entire proposed size:

Code
ScrollView {    LazyVGrid(columns: [                  .init(.adaptive(                            minimum: 
)) ]) { ColoredBoxes() } .padding(8)}
Preview
200

Spacing

There are two kinds of spacing we can adjust. The first is the row spacing, which is a property of the grid itself. The second is the spacing between columns, or to be more precise, the spacing after a column (all columns except the last have spacing after them). In the example below, we have one fixed column and one adaptive column (which expands to multiple columns). Each have their own spacing values. The grid's row spacing can be adjusted as well.

Code
ScrollView {    LazyVGrid(columns: [                  .init(.fixed(50),                        spacing: 
),
.init(.adaptive(minimum: 80), spacing:
)
], spacing:
) {
ColoredBoxes() } .padding(8)}
Preview
300

Column Layout

The column layout algorithm runs in two steps. It takes the remaining width, and distributes it among the columns, similar to HStack. These are the steps to distribute the space:

Each of the column types responds differently to the proposed width:

After all columns have a width, the grid will expand the adaptive columns. For each adaptive column, it will take the column's width and try to fit in as many columns as possible (given the constraints).

In the example below, we can see how the grid calculates the column widths and then fills the adaptive column with as many columns as possible. The flexible column becomes the same width as the adaptive column (when there is enough space available).

Code
ScrollView {    LazyVGrid(columns: [                  .init(.fixed(50)),                  .init(.adaptive(                            minimum: 
)), /* .init(.flexible(minimum: 30)) */ ]) { ColoredBoxes() } .padding(8)}
Preview

ViewColumns
300

This behavior can be quite confusing. In general, we recommend either having adaptive columns or regular columns, but not both.

Gotchas

There are a few more subtleties to the layout algorithm. For example, during proposing and reporting, the proposed width is used to compute the column widths, and the reported width is the sum of all the columns. However, during the layout phase, the reported width of the grid is used to compute the column widths once again. Because the reported width might be larger than the proposed width this can lead to some surprising results.

In the example below, the grid is proposed a width of 200 and has 10 points spacing between the two columns. The first column gets proposed 95 points. The second column gets proposed 95 points as well but becomes 120 points wide, applying its minimum width. The reported size of the grid is 95 + 10 + 120 = 225 points. During layout, the 225 points are used to compute the column widths again. The first column becomes (225 - 10) / 2 = 107.5 points. The second column gets proposed the remainder (107.5) and becomes 120 points wide.

Code
LazyVGrid(columns: [              .init(.flexible(minimum: 50),                    spacing: 10),              .init(.flexible(minimum: 120))          ]) {    ColoredBoxes()}.frame(width: 200).border(Color.red)
Preview

More Resources

Share Image

In this excellent article, Javier takes us through the full API of LazyVGrid. He also shows some advanced examples of building complex layouts.

https://swiftui-lab.com/impossible-grids/
Share Image

In this episode of Swift Talk, we build a photo grid using LazyVGrid. It also shows how to use matched geometry effect for a smooth transition between a full-screen photo and the thumbnails.

http://talk.objc.io/episodes/S01E314-building-a-photo-grid-refactoring
Share Image

The official documentation for LazyVGrid.

https://developer.apple.com/documentation/swiftui/lazyvgrid
Share Image

In this article Florian shows how a lazy grid lays out its columns through a series of quiz questions.

https://www.objc.io/blog/2020/11/23/grid-layout/