Grids in SwiftUI
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.
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.
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: Stringlet numberOfDays: Intvar days: [Day]init(name: String, numberOfDays: Int) {self.name = nameself.numberOfDays = numberOfDaysself.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 inCapsule().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
.
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 inCapsule().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.
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:
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))]
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))]
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:
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))]
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 inSection(header: Text(verbatim: month.name).font(.headline)) {ForEach(month.days) { day inCapsule().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.
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 inSection(header: Text(verbatim: month.name).font(.headline)) {ForEach(month.days) { day inCapsule().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
toLazyHGrid
- We changed
columns:
torows:
- We changed the
.frame
of theCapsule
to specify width instead of height
And voila, our grid is now laid out horizontally with seven flexible height rows:
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 inSection(header: Text(verbatim: month.name).font(.headline)) {ForEach(month.days) { day inCapsule().overlay(Text("\(day.value)").foregroundColor(.white)).foregroundColor(.blue).frame(height: 40)}}}}}}
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.
Author
'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'
Meet the full teamRelated articles
- Article
Building Lists in SwiftUI
In this article we learn how to create list views in our SwiftUI apps using the `List` structure. We will cover how to populate the contents of the list using a dynamic data source. - Article
Forms and Pickers
Learn how to create forms with pickers and other controls in SwiftUI.