Exploring the SwiftUI’s Grid View

LazyVGrid & GridItem and Grid & GridRow

Rohit Kumar
9 min readFeb 29, 2024

Introduction

In SwiftUI, we can create a two-dimensional responsive list using LazyVGrid or LazyHGrid views. If we want a vertical grid, we can use the LazyVGrid view, and if we want a horizontal grid, we can use the LazyHGrid view. These views allow us to create a grid of items that adapt to different orientations and screen sizes.

With the introduction of iOS 16, apple introduces a new way to statically arrange views in two dimensions. We can also create a two-dimensional layout by initializing a Grid with a collection of GridRow structures.

This article will cover the following topics :-

  • Fundamentals of grid-based layouts in SwiftUI using LazyVGrid
  • Configure the layout of items in a lazy grid using GridItem with GridItem(.adaptive(minimum: 50, maximum: 100)), GridItem(.fixed(50)) and GridItem(.flexible())
  • A new way to statically arrange views in two dimensions using Grid and GridRow. Customizing Grid using Multicolumn cells, Cell spacing and alignment.
    - Why Grid ?
    - Grid Customization with gridCellUnsizedAxes(_:), gridCellColumns(_:), gridColumnAlignment(_:), gridCellAnchor(_:)

Let’s start with a basic example of using LazyVGrid:

struct ContentView: View {
let data = Array(1...50)

var columns: [GridItem] = [
GridItem(.fixed(50)),
GridItem(.fixed(50)),
GridItem(.fixed(50))
]

var body: some View {
ScrollView {
LazyVGrid(columns: self.columns) {
ForEach(data, id: \.self) { number in
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.orange)
.foregroundColor(Color.black)
.cornerRadius(25)
}
}
}
.padding()
}
}
LazyVGrid of three colums using fixed-size GridItem

In the above example, we create a grid of Textnumbered from 1 to 50 using LazyVGrid. First, defined a number of columns with GridItem. Here, we define three columns with a fixed size of width 50 by setting .fixed(50).

Then, we put the grid view inside a scroll view to make it scrollable. And last, specified columns as an argument for LazyVGrid. That’s all we need to do to create a grid layout in SwiftUI.

GridItem

GridItem is basically a description of a row or a column in a lazy grid. To configure the layout of items in a lazy grid, we use an array of GridItem instances (columns in case of LazyVGrid).

Each grid item in the array specifies layout properties like size and spacing for the columns of a LazyVGrid or the rows of a LazyHGrid. We will look about spacing in grid item later on.

// Minimum item width of 50
GridItem(.adaptive(minimum: 50))

// Fixed item width of 50
GridItem(.fixed(50))

// Flexible item width
GridItem(.flexible())

If we want the grid to fit in as many items per row as possible, we can use the adaptive() size modifier. By setting .adaptive(minimum: 50), we will have the multiple number of Text items in a row with a minimum size of 50 points each.


struct ContentView: View {
let data = Array(1...50)

var columns: [GridItem] = [
GridItem(.adaptive(minimum: 50)) // Minimum item width of 50
]

var body: some View {
ScrollView {
LazyVGrid(columns: self.columns) {
ForEach(data, id: \.self) { number in
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.orange)
.foregroundColor(Color.black)
.cornerRadius(25)
}
}
}
.padding()
}
}
LazyVGrid with colums using adaptive-size GridItem

If we want to control the number of items in each column, we can use the flexible() size modifier. By using the flexible() modifier, we can control the number of items in each column. However, if there isn’t enough space to accommodate the minimum size of the view, items may overlap.

struct ContentView: View {
let data = Array(1...50)

var columns: [GridItem] = [
GridItem(.flexible(minimum: 50, maximum: 100)),
GridItem(.flexible(minimum: 50, maximum: 100)),
GridItem(.flexible(minimum: 50, maximum: 100))
]

var body: some View {
ScrollView {
LazyVGrid(columns: self.columns) {
ForEach(data, id: \.self) { number in
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.orange)
.foregroundColor(Color.black)
.cornerRadius(25)
}
}
}
.padding()
}
}
LazyVGrid of three colums using flexible-size GridItem

⚠️ We can control the item width of LazyVGrid, but the height is determined by the highest item in that particular row. Similarly, we can control the item height of LazyHGrid, but the width is determined by the widest item in that particular column.

Spacing in Grid Item

spacing define spacing to the next item. If this value is nil, the item uses a reasonable default for the current platform.

Here is an example of a five-column grid where the spacing between each item in a column is 0, 10, 20, and 30, respectively.

Note that the spacing defined in the last column (40) has no effect since there is no next item.

struct ContentView: View {
let data = Array(1...50)

var columns: [GridItem] = [
GridItem(.fixed(50), spacing: 0),
GridItem(.fixed(50), spacing: 10),
GridItem(.fixed(50), spacing: 20),
GridItem(.fixed(50), spacing: 30),
GridItem(.fixed(50), spacing: 40)
]

var body: some View {
ScrollView {
LazyVGrid(columns: self.columns) {
ForEach(data, id: \.self) { number in
Text("\(number)")
.frame(height: 50)
.frame(maxWidth: .infinity)
.background(Color.orange)
.foregroundColor(Color.black)
.cornerRadius(25)
}
}
}
.padding()
}
}
LazyVGrid with five colums using grid items with spacing of 0, 10, 20, 30 and 40.

Alignment in Grid Item

The alignment to use when placing each view. Use this property to anchor the view’s relative position to the same relative position in the view’s assigned grid space.

By default, each item will center align within a row/column. We can change this with the alignment property.

Here is an example of a six-column grid where the alignment of each item is set to default, top, center, bottom, topLeading and bottomTrailing respectively.

struct ContentView: View {
let data = Array(1...50)

var columns: [GridItem] = [
GridItem(.fixed(60)),
GridItem(.fixed(80), alignment: .top),
GridItem(.fixed(80), alignment: .center),
GridItem(.fixed(80), alignment: .bottom),
GridItem(.fixed(80), alignment: .topLeading),
GridItem(.fixed(80), alignment: .bottomTrailing),
]

var body: some View {
ScrollView {
LazyVGrid(columns: self.columns) {
ForEach(data, id: \.self) { number in
Text("Item \(number)")
.foregroundColor(Color.black)
.padding(8)
.background(Color.orange)
.border(.black)
}
}
}
.padding()
}
}
Alignment in Grid Item

Grid and GridRow

With the introduction of iOS 16, apple introduces a new way to statically arrange views in two dimensions. Create a two-dimensional layout by initializing a Grid with a collection of GridRow structures. The first view in each grid row appears in the grid’s first column, the second view in the second column, and so on.

Let’s start with a basic example that creates a grid with two rows and two columns:

struct ContentView: View {
var body: some View {
Grid {
GridRow {
Text("Hello")
Image(systemName: "globe")
}
GridRow {
Image(systemName: "hand.wave")
Text("World")
}
}
}
}
a grid with two rows and two columns

Multicolumn cells

The GridRow controls the number of rows we want to display in the Grid View. Each element within the GridRow corresponds to a column element. If you create rows with different numbers of columns, the grid adds empty cells to the trailing edge of rows that have fewer columns. The example below creates three rows with different column counts:

struct ContentView: View {
var body: some View {
Grid {
GridRow {
Text("Row 1")
ForEach(0..<2) { _ in Color.red }
}
GridRow {
Text("Row 2")
ForEach(0..<5) { _ in Color.green }
}
GridRow {
Text("Row 3")
ForEach(0..<4) { _ in Color.orange }
}
}
.padding()
}
}

Cell spacing and alignment

We can control the spacing between cells in both the horizontal and vertical dimensions, and set a default alignment for the content in all the grid cells when we initialize the grid. Consider a modified version of the previous example:

Grid(alignment: .bottom, horizontalSpacing: 20, verticalSpacing: 2) {
// ...
}
A grid with bottom alignment, horizontal apacing of 20 and vertical spacing of 2. The bottom alignment only affects the text cells because the colors fill their cells completely

Why Grid ?

You may be wondering by now that what’s the use of Grid if we already have LazyVGrid and LazyHGrid. Till now, we have only seen the basics of Grid which have same functionalities as of LazyVGrid and LazyHGrid.

But, what’s more in Grid ?
Grid enables us to organize views in a table-like structure, similar to what we do in web. We can merge cells or columns, create blank cells, set cell spacing, and control alignment and other things. Following are the available methods for achieving this. Let see all of them one by one.

  • gridCellUnsizedAxes(_:)
  • gridCellColumns(_:)
  • gridColumnAlignment(_:)
  • gridCellAnchor(_:)

gridCellUnsizedAxes(_:)

Use this modifier to prevent a flexible view from taking more space on the specified axes than the other cells in a row or column require.

If we add an item inside Grid view without GridRow, it will expand to a full column, i.e. it will takes as much space as its parent offers.

struct ContentView: View {
var body: some View {
Grid {
GridRow {
Text("Hello")
Image(systemName: "globe")
}

// This Divider will takes as much space as its parent offers
Divider()

GridRow {
Image(systemName: "hand.wave")
Text("World")
}
}
}
}
The Divider takes as much space as its parent offers, the grid fills the width of the display, expanding all the other cells to match

Here, how we can prevent the grid from giving the divider more width than the other cells require by adding the gridCellUnsizedAxes modifier:

Divider()
.gridCellUnsizedAxes(.horizontal)
Divider added with gridCellUnsizedAxes modifier

We can also utilize gridCellUnsizedAxes to create an empty cell in a grid.

struct NContentView: View {
var body: some View {
Grid {
GridRow {
ForEach(0..<3) { _ in Rectangle().foregroundColor(.red) }
}
GridRow {
Rectangle().foregroundColor(.green)
Rectangle()
.foregroundColor(.clear)
.gridCellUnsizedAxes([.horizontal, .vertical])
Rectangle().foregroundColor(.green)
}
GridRow {
ForEach(0..<3) { _ in Rectangle().foregroundColor(.orange) }
}
}
.padding()
}
}

gridCellColumns(_:)

By default, each view that you put into the content closure of a GridRow corresponds to exactly one column of the grid. If you want a view to span more than one column, you can specify that using gridCellColumns(_:) modifier.

struct NContentView: View {
var body: some View {
Grid {
GridRow {
ForEach(0..<3) { _ in Rectangle().foregroundColor(.red) }
}
GridRow {
Rectangle().foregroundColor(.green)
Text("This text will span to two columns")
.gridCellColumns(2) // Span two columns.
}
GridRow {
ForEach(0..<3) { _ in Rectangle().foregroundColor(.orange) }
}
}
.padding()
}
}
A grid view using gridCellColumns modifier to span text view to two columns

gridColumnAlignment(_:)

We can use the gridColumnAlignment(_:) modifier to override the horizontal alignment of a column within the grid. It causes all cells in the same column of a grid to use the same alignment.

struct ContentView: View {
var body: some View {
Grid {
GridRow {
Rectangle().foregroundColor(.green)

// Align the entire second column.
Text("Trailing text")
.gridColumnAlignment(.trailing)

Rectangle().foregroundColor(.green)
}
GridRow {
Text("Row 2, Col 1")
Text("Row 2, Col 2")
Text("Row 2, Col 3")
}
GridRow {
ForEach(0..<3) { _ in Rectangle().foregroundColor(.orange) }
}
}
.padding()
}
}
A grid using gridColumnAlignment modifier

gridCellAnchor(_:)

When we use the gridCellAnchor(_:) modifier on a view in a grid, the grid changes to an anchor-based alignment strategy for the associated cell. With anchor alignment, the grid projects a UnitPoint that we specify onto both the view and the cell, and aligns the two projections.
For example, consider the following grid:

struct ContentView: View {
var body: some View {
Grid {
GridRow {
Rectangle().foregroundColor(.green).frame(width: 100, height: 100)
Rectangle().foregroundColor(.green).frame(width: 100, height: 100)
}
GridRow {
Rectangle().foregroundColor(.orange).frame(width: 100, height: 100)
Rectangle()
.foregroundColor(.orange)
.frame(width: 20, height: 20)
.gridCellAnchor(UnitPoint(x: 0.25, y: 0.75))
}
}
.padding()
}
}
A grid using gridCellAnchor modifier

With the anchor modifier shown in the code above, the grid aligns the one quarter point of the marker with the one quarter point of its cell in the x direction, as measured from the origin at the top left of the cell. The grid also aligns the three quarters point of the marker with the three quarters point of the cell in the y direction.

We can also use the typical alignment guides for gridCellAnchor modifier. Consider a modified version of the previous example:

Rectangle()
.foregroundColor(.orange)
.frame(width: 20, height: 20)
.gridCellAnchor(.topTrailing)
A grid using gridCellAnchor modifier

👉 Applying the anchor-based alignment strategy to a single cell doesn’t affect the alignment strategy that the grid uses on other cells.

Conclusion

Grid view render all of its child views all at once. If you want better performance of your app, you can use LazyHGrid or LazyVGrid.

Thanks for Reading!

Hope, you have found this article useful. If you liked this article, please share and 👏 so other people can read it too.

For reading & learning about these topics, I found the Apple’s documentation to be very useful. Here are the reference for Grid view and LazyVGrid & LazyHGrid.

--

--

Rohit Kumar
Rohit Kumar

Written by Rohit Kumar

SDE @Yellow.ai | Swift, UIKIt, SwiftUI

No responses yet