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.
ScrollView { LazyVGrid(columns: [ .init(.flexible(minimum: 40)), .init(.fixed(60)) ]) { ColoredBoxes() } .padding(8)}
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:
ScrollView { LazyVGrid(columns: [ .init(.adaptive( minimum: ), spacing: 2) ], spacing: 2) { PhotoPlaceholders() }}
Here is a similar example, but with cells that fill the entire proposed size:
ScrollView { LazyVGrid(columns: [ .init(.adaptive( minimum: )) ]) { ColoredBoxes() } .padding(8)}
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.
ScrollView { LazyVGrid(columns: [ .init(.fixed(50), spacing: ), .init(.adaptive(minimum: 80), spacing: ) ], spacing: ) { ColoredBoxes() } .padding(8)}
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:
- For each column but the last, the spacing is subtracted from the remaining width.
- The columns are given their width in order, but all the fixed width columns come first.
- For each column, we propose the remaining width divided by the number of remaining columns, and subtract the returned column width from the remainder.
Each of the column types responds differently to the proposed width:
- A fixed column discards the proposed width and always becomes the fixed size.
- An adaptive column accepts the proposed width.
- A flexible column becomes the proposed width, clamped to its constraints.
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).
ScrollView { LazyVGrid(columns: [ .init(.fixed(50)), .init(.adaptive( minimum: )), /* .init(.flexible(minimum: 30)) */ ]) { ColoredBoxes() } .padding(8)}
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.
LazyVGrid(columns: [ .init(.flexible(minimum: 50), spacing: 10), .init(.flexible(minimum: 120)) ]) { ColoredBoxes()}.frame(width: 200).border(Color.red)
More Resources
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/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-refactoringThe official documentation for LazyVGrid.
https://developer.apple.com/documentation/swiftui/lazyvgridIn 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/