Grids in SwiftUI

Codecademy Team
In this article, we will introduce the SwiftUI Grid structure and walk through how to leverage it to implement scrollable horizontal and vertical grid layouts.

Introduction

The grid layout has become a familiar paradigm for mobile user experiences. The canonical example of grid layout is in the built-in camera roll app for iOS. It can also be found on an extremely popular social photo-sharing app in the feature called (unsurprisingly) the grid. It is an intuitive, efficient means for presenting large collections of views to the user.

Figure 0

Luckily, SwiftUI gives us a native structure for creating impressive grid layouts with only a few lines of expressive code. The Grid structure is highly performant and provides helpful functionality so that your scrollable grid layout can adapt to various device sizes and orientations. Without further ado, let’s experience the power of grids by building some in SwiftUI!

Create the project

To explore the power and flexibility of the Grid structure, we’ll create an app that presents a calendar view. Let’s start by creating a new project in Xcode called “GridCalendar”. We won’t need to use Core Data or include tests.

Figure 1

We should now be at the starting point of a standard SwiftUI project with the `ContentView.swift’ file open:

![Figure 2)

Creating the data structures for the calendar grid

We’re going to start by making a couple of structs that will represent months and days of a year. We’ll populate each month with the correct number of days, and display it all in a grid view. Define the Day structure by adding the following code underneath the ContentView structure:

struct Day: Identifiable {
let id = UUID()
let value: Int
}

The value constant will be used to store the order of each particular day of the month, i.e. the 2nd of June will have a value of 2. Since we’ll be using a ForEach structure to iterate through the days, we’ll need to assign a unique identifier to them. It may be tempting to use the value as the unique identifier, but this won’t work since we will be iterating through all of the months so there will be duplicated values across all of the months, i.e. there is an April 10th as well as an October 10th.

We make the Day structure conform to the Identifiable protocol, which simply requires that it has an id value that is unique. We assign id by initializing a UUID structure, which is built into Foundation and guaranteed to be a unique value.

Next, we’ll define the Month structure by adding the following code underneath the day definition:

struct Month {
let name: String
let numberOfDays: Int
var days: [Day]
init(name: String, numberOfDays: Int) {
self.name = name
self.numberOfDays = numberOfDays
self.days = []
for n in 1...numberOfDays {
self.days.append(Day(value: n))
}
}
}

We’ll want to display the name of each month in our calendar, so we’ll store that in the name constant. The numberOfDays constant will hold the correct number of days for each month. We’ll want to iterate over an array of Day structures, so we’ll store that in the days array. When we initialize each Month structure, we’ll generate the days array on the fly based on the numberOfDays for that particular month. So we create a custom initializer that assigns the correct values of name and numberOfDays to itself and initializes days with an empty array. Then we use a for loop to iterate over a range from 1 to numberOfDays, initialize a Day structure for each value in the range, and append that structure to the days array.

Now that we have defined structures to represent the months and days of our calendar, we have laid the foundation to build an array that will represent the entire year. Add the following code underneath the Month structure:

let year = [
Month(name: "January", numberOfDays: 31),
Month(name: "February", numberOfDays: 28),
Month(name: "March", numberOfDays: 31),
Month(name: "April", numberOfDays: 30),
Month(name: "May", numberOfDays: 31),
Month(name: "June", numberOfDays: 30),
Month(name: "July", numberOfDays: 31),
Month(name: "August", numberOfDays: 31),
Month(name: "September", numberOfDays: 30),
Month(name: "October", numberOfDays: 31),
Month(name: "November", numberOfDays: 30),
Month(name: "December", numberOfDays: 31),
]

To build the year array we initialize a Month structure for each month in a year and pass in the correct number of days for each month.

Awesome, now we have structured data that will form the informational architecture of our calendar grid view!

Introducing GridItem and LazyVGrid

In order to create our grid-based view, we’ll need to define an array of GridItem instances first. These GridItem instances serve to describe the layout properties of the grid. We can use various types of GridItem instances to get the spacing and alignment of the columns or rows of the grid exactly how we wish. We can even use them to describe a grid that adjusts dynamically based on the size and orientation of the device. Truly the best way to grok them is to see them in action.

Let’s add a constant named layout inside of our ContentView structure, right over the body variable:

let layout = [
GridItem(.fixed(40))
]

Defining a single GridItem like this will create a single fixed column of width 40, when used as input for a LazyVGrid. As you may have suspected, the V in LazyVGrid stands for vertical. When used in a LazyHGrid (a horizontal grid) it will create a single fixed row of height 40.

Now we’re finally ready to see our grid in action! Replace the contents of the body var so that it looks like this:

var body: some View {
LazyVGrid(columns: layout) {
ForEach(year[0].days) { day in
Capsule()
.overlay(Text("\(day.value)").foregroundColor(.white))
.foregroundColor(.blue)
.frame(height: 40)
}
}
}

Let’s break down what’s going on in that code snippet. We pass the layout constant we created in the earlier step as an argument to define the columns for our LazyVGrid structure. Then we get the value of the 0 index of the year array, which is equal to the Month of January. We iterate through the days property of that Month instance, which is an array of Day instances that was created when we initialized that Month. For each Day, we create a Capsule with overlay text content that displays a string equal to the value property of the Day.

Figure 3

Who is lazy?

You might be curious why the grid view is named LazyVGrid. Similar to the concept of lazy properties, the Lazy in LazyVGrid and LazyHGrid refers to the fact that the elements of the grid aren’t created until they are needed to display in the view. This is a smart strategy because in practice we might want to create grids with a large count of complex items. Loading thousands of images would cause the app to slow down. By using lazy loading, we create much smoother, more performant user experiences.

Make the grid scroll

As we expected, we have a vertical grid layout with one column of fixed width! However, there is one slight issue: we can’t see all of the content items of the grid. It would be great if there was a way to scroll the view. Luckily with SwiftUI, all we need to do is wrap our LazyVGrid with a ScrollView. Modify your body variable so it looks like this:

var body: some View {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(year[0].days) { day in
Capsule()
.overlay(Text("\(day.value)").foregroundColor(.white))
.foregroundColor(.blue)
.frame(height: 40)
}
}
}
}

If you’re using the live preview, you might have to click the play button to make the view respond to user interaction.

Figure 4

Aha! The view is free to scroll about the window 🕊

Exploring layout with GridItem

But you may be thinking, this isn’t really a grid, it’s more of a list. Let’s explore the power of GridItem to create incredible, adaptable grid layouts! Change the layout constant so that we have four instances of GridItem like so:

let layout = [
GridItem(.fixed(40)),
GridItem(.fixed(40)),
GridItem(.fixed(40)),
GridItem(.fixed(40))
]

As you might expect, this gives us four columns of fixed width:

Figure 5

Now let’s change the last GridItem to use the flexible option:

let layout = [
GridItem(.fixed(40)),
GridItem(.fixed(40)),
GridItem(.fixed(40)),
GridItem(.flexible(minimum:40))
]

Figure 6

The flexible option creates a column that takes up the available width it has. You must define a minimum width, and you can optionally specify a maximum width. Finally, there is also an adaptive option that fits multiple columns inside the width of a single GridItem. This can be super helpful when designing layouts that adapt to phones and tablets of varying sizes and orientations. Modify the layout constant so it looks like this:

let layout = [
GridItem(.fixed(40)),
GridItem(.fixed(40)),
GridItem(.fixed(40)),
GridItem(.adaptive(minimum: 80))
]

Figure 7

The adaptive GridItem fills up the remaining width with three columns of at least width 80 in this case. But if we rotate the device to landscape orientation we can see the layout dynamically adapts:

Figure 8

Since we’re building a calendar view, let’s use seven flexible grid items like so:

let layout = [
GridItem(.flexible(minimum: 40)),
GridItem(.flexible(minimum: 40)),
GridItem(.flexible(minimum: 40)),
GridItem(.flexible(minimum: 40)),
GridItem(.flexible(minimum: 40)),
GridItem(.flexible(minimum: 40)),
GridItem(.flexible(minimum: 40))
]

Figure 9

Adding sections to the grid

So far we have been looking at the first month of the year, but in our calendar we’d like to view all of the months at the same time. We can use the Section structure to separate each month into a section with its own title. Modify your body to look like this:

var body: some View {
ScrollView {
LazyVGrid(columns: layout) {
ForEach(year, id: \.name){ month in
Section(header: Text(verbatim: month.name).font(.headline)) {
ForEach(month.days) { day in
Capsule()
.overlay(Text("\(day.value)").foregroundColor(.white))
.foregroundColor(.blue)
.frame(height: 40)
}
}
}
}
}
}

We wrapped the ForEach we had with a Section view that gives us a header view that we can use to display the name of the month. Then we wrapped the Section with another ForEach that iterates through all of the months of the year. Remember that we didn’t make the Month structure conform to Identifiable, so we need to specify a unique key path in the ForEach structure. As long as we don’t have the same name of the month in our year, we can use it as the unique id.

Figure 10

Pinned views

Another useful feature of grids is the ability to pin views while scrolling. Modify the definition of your LazyVGrid like so:

LazyVGrid(columns: layout, pinnedViews: [.sectionHeaders]) {

Now you can see the section header view sticks to the top of the screen while you are scrolling through that particular section. This can help create a smooth, intuitive user experience.

Horizontal grid views

There may be situations where you want the grid to be laid out in horizontal rows instead of vertical columns. Changing this configuration is trivial with SwiftUI! Modify your body variable so it looks like this:

var body: some View {
ScrollView(.horizontal) {
LazyHGrid(rows: layout, pinnedViews: [.sectionHeaders]) {
ForEach(year, id: \.name){ month in
Section(header: Text(verbatim: month.name).font(.headline)) {
ForEach(month.days) { day in
Capsule()
.overlay(Text("\(day.value)").foregroundColor(.white))
.foregroundColor(.blue)
.frame(width: 40)
}
}
}
}
}
}

We made four modifications:

  • We changed the ScrollView to the horizontal option
  • We changed LazyVGrid to LazyHGrid
  • We changed columns: to rows:
  • We changed the .frame of the Capsule to specify width instead of height

And voila, our grid is now laid out horizontally with seven flexible height rows:

Figure 11

In the case of our calendar, it definitely makes more sense to display it in a vertical grid. Let’s change it back to vertical:

var body: some View {
ScrollView {
LazyVGrid(columns: layout, pinnedViews: [.sectionHeaders]) {
ForEach(year, id: \.name){ month in
Section(header: Text(verbatim: month.name).font(.headline)) {
ForEach(month.days) { day in
Capsule()
.overlay(Text("\(day.value)").foregroundColor(.white))
.foregroundColor(.blue)
.frame(height: 40)
}
}
}
}
}
}

Figure 12

Conclusion

In this article, we explored the power and flexibility of the LazyVGrid and LazyHGrid structures by building a simple calendar app. We saw how to use GridItem to generate highly customizable grid layouts. We learned how to make the grid scrollable and how to add pinnable sections to the view. You can download the complete code for the application at the link here.

As a challenge exercise, you might try to add functionality to the calendar to make it more realistic, such as labels for the days of the week (Sunday – Monday) and making the dates line up accurately with the labels given a specific year.