16.
Grids
Written by Bill Morefield
Stacks and lists provide a one-dimensional arrangement of views. Many data types better fit a two-dimensional grid. A significant weakness of the initial release of SwiftUI was the lack of a native collection view. This view is so helpful that the first two editions of this book included a chapter that walked through creating a reusable grid view. SwiftUI 2.0 added a native grid view. In this chapter, you’ll examine and work with grid layouts in SwiftUI.
Building Grids the Original Way
The containers in the original SwiftUI version that let you organize other views shared one thing in common; they work in one dimension. Stacks create horizontal or vertical layouts. Lists create vertical layouts.
You can think of a grid as a set of stacks in one direction wrapped within a stack of the other direction. Because of this, you can create more complex layouts, even with these limitations. You just had to do the work yourself.
Open the starter project and run the app. Earlier editions of this book changed the buttons on the welcome screen to a grid, but that no longer fits the new split navigation design (see Chapter 13: Navigation). Instead, you’ll explore grids using the new awards view added to the starter project. Tap the Your Awards button to look at the initial view.
This view displays the user’s current awards and those the user hasn’t received yet. Currently, the list shows as a vertical stack using a LazyVStack. Open AwardsView.swift under the AwardsView group and change the closure of the ScrollView to:
// 1
VStack {
// 2
HStack {
NavigationLink(value: awardArray[0]) {
AwardCardView(award: awardArray[0])
.foregroundColor(.black)
.frame(width: 150, height: 220)
}
Spacer()
NavigationLink(value: awardArray[1]) {
AwardCardView(award: awardArray[1])
.foregroundColor(.black)
.frame(width: 150, height: 220)
}
}
// 3
HStack {
AwardCardView(award: awardArray[2])
.foregroundColor(.black)
.frame(width: 150, height: 220)
Spacer()
AwardCardView(award: awardArray[3])
.foregroundColor(.black)
.frame(width: 150, height: 220)
}
Spacer()
}
.font(.title)
.foregroundColor(.white)
.padding()
That’s a lot of code but focus on the layout views. You’ll see that you’re building a grid by nesting two HStacks inside a VStack to show the first four awards.
- Using an initial
VStackcreates the overall vertical layout of the grid. - This
HStackbuilds the first row of the grid. It contains two of the button views separated by aSpacer. Note that you manually reference the elements of theawardArrayarray. - This
HStackmakes the second row of the grid.
Run the app, and you’ll see the grid.
Before the Grid view, this technique was the only way to build a grid. You can see how complex it becomes as you add more items to the grid. This book’s first edition included a chapter on creating a generic reusable grid that you can consult if you want to see more of this technique. With the second release of SwiftUI, there’s now a native and more flexible way to build a grid. You’ll change the app to use that in the next section.
Creating a Fixed Column Grid
The native SwiftUI grid view builds on the existing LazyHStack and LazyVStack views. As with stacks, there are two grids, one that grows horizontally and one that grows vertically. Change the contents of the ScrollView in AwardsView.swift to:
// 1
LazyVGrid(
// 2
columns:
[
// 3
GridItem(.fixed(160)),
GridItem(.fixed(160))
],
// 4
spacing: 15
) {
// 5
ForEach(awardArray) { award in
// 6
NavigationLink(value: award) {
AwardCardView(award: award)
.foregroundColor(.black)
// 7
.frame(width: 150, height: 220)
}
}
}
.navigationDestination(for: AwardInformation.self) { award in
AwardDetails(award: award)
}
.font(.title)
.foregroundColor(.white)
.padding()
Notice this doesn’t look that different from the initial LazyVStack. That’s the beauty of SwiftUI’s approach to a grid building off these one-dimensional views. To change the stack to a grid:
- The
LazyVGridbuilds a set of rows that extend vertically. The correspondingLazyHGridcreates a list of rows that extends horizontally. - The new parameter for a vertical grid is
columns. You pass it an array ofGridItemelements that describes the columns. - The array consists of a set of
GridItems to describe the grid. Here you use the simplest type ofGridItem, a fixed column you set 160 points wide. - You also pass the optional
spacingparameter. This parameter sets the space between the rows of the grid. It does not affect the distance between the columns of the grid. - Thanks to the grid, you can use
ForEachto iterate over theawardArrayarray as it separates the elements of the grid in the closure to theLazyVGridfrom the layout defined using thecolumnsparameter to theLazyVGrid. - Each item in the grid will be a
NavigationLink, which sets the value to theawardpassed to the closure. It displays theAwardCardViewfor the award as the view. - You set the width of the award car to 150 points wide. This width leaves a ten-point margin compared to the 160 points column width you set in step two.
You’ll see no need to manually layout each row as you did when building a grid with nested view stacks. You also no longer need to worry about keeping the grid lined up. SwiftUI takes care of those concerns for you.
Note: The code for this chapter uses a vertical grid where you define columns. Everything you’ll do also works in a horizontal grid, except you would pass
rowsand pass in descriptions of the grid’s rows. TheGridItemworks for both.
Run the app, tap **. You’ll see the grid looks similar to that from the last section, but with smaller columns since you set them to 160 points.
Now that’s you’ve seen the basics of grids, you’ll look at more flexible layouts in the next section.
Building Flexible Grids
You often want more flexibility when creating columns (or rows) in your grid. A flexible element in a grid lets you specify a range of sizes to constrain a grid while setting the number of rows or columns in the grid. Placing the column information inside the view clutters its view, especially when your grid becomes more complicated. Instead, you will specify the column structure using a property. Add the following code after the awardArray property:
var awardColumns: [GridItem] {
[
GridItem(.flexible(minimum: 150)),
GridItem(.flexible(minimum: 150))
]
}
This property returns an array of two GridItem elements. Since the array contains two elements, SwiftUI will create a grid of two columns.
A flexible grid item lets you specify the minimum or maximum width for each column or both. Here you only define the minimum at 150 points. Since you don’t specify a maximum width, the column can grow as large as needed to handle the content.
Now change the LazyVGrid to:
LazyVGrid(columns: awardColumns, spacing: 15) {
ForEach(awardArray) { award in
NavigationLink(value: award) {
AwardCardView(award: award)
.foregroundColor(.black)
.frame(width: 150, height: 220)
}
}
}
Make sure not to lose any of the modifiers attached to the LazyVGrid. You replaced the inline columns with the awardColumns property you previously added to define the grid columns. Run the app and tap Your Awards. The awards still show in a two-column grid but are no longer bound to a fixed size.
In the next section, you’ll learn how this more flexible layout works with different sized views.
Interacting Between Views and Columns
It’s worth exploring how the container view’s size interacts with the settings for columns in the grid. Change the frame for the award card to:
.frame(width: 190, height: 220)
What effect do you think this will have on the grid? When you have an answer, run the app and go to the awards grid to see if you’re correct:
Since you specified a flexible column with only a minimum size constraint, the column expands to accommodate the larger card width. On many phones, such as the one in the screenshot, this larger size will not accommodate the full width of two cards. The cards will overlap as SwiftUI tries to fit the views.
What happens if you specify a maximum column width that’s smaller than this new card width?
Change the awardColumns property to:
var awardColumns: [GridItem] {
[
GridItem(.flexible(minimum: 100, maximum: 150)),
GridItem(.flexible(minimum: 100, maximum: 150))
]
}
Change the frame for the award card to:
.frame(width: 160, height: 240)
Note: The preview canvas will not always notice when you change a property. If you see no change, then hide and restore the canvas to force it to refresh.
You specified a maximum width of the column of 150 points and changed the card to a width of 160 points. Run the app and view the awards to see the effect.
As you might expect, your grid remains a bit cramped. The key to notice is that column does not clip or constrain its containing view to the column. Here, both columns take the full 160 points width despite the column having a maximum width of 150 points.
How do you size views to a grid without running into these concerns? Since you’re specifying a size for the grid columns, you might ask whether you need to specify a frame for the card at all. The answer is no, and not doing so lets SwiftUI adjust the size to fit the containing view better.
Removing the subview’s frame modifier creates a side effect that you no longer control the dimensions. You can use a different modifier to keep the shape of the view. Change the awardColumns property to:
var awardColumns: [GridItem] {
[
GridItem(.flexible(minimum: 100, maximum: 160)),
GridItem(.flexible(minimum: 100, maximum: 160))
]
}
Now change the award card view to:
AwardCardView(award: award)
.foregroundColor(.black)
.aspectRatio(0.67, contentMode: .fit)
You now have a much more flexible view that can adjust to different widths while maintaining its overall shape. You use aspectRatio(_:contentMode:) to set the desired ratio of the view’s width to its height — in this case, a view three points tall for every 2 points wide — and tell SwiftUI to fit the view to this aspect ratio.
A limitation on a flexible grid item can be a boon or a problem depending on your app. Run the app and go to the award view. Now rotate the device or simulator, and you’ll see that the grid still only shows two columns with lots of space on both sides. You would see the same result when viewing the grid on a larger device such as an iPad.
Specifying the number of columns for a grid means you’re stuck with that number of columns, even when you have space for more. You could increase the number of columns, which would crowd the views on a smaller display or require you to create different arrays for different devices.
To fill this need, SwiftUI provides a third type of GridItem, the adaptive column.
Building Adaptive Grids
The adaptive grid provides you the most flexible option. Using one tells SwiftUI to fill the space with as many columns or rows as fit in the grid. Change the awardColumns property to:
var awardColumns: [GridItem] {
[GridItem(.adaptive(minimum: 150, maximum: 170))]
}
Run the app, and you’ll see the view looks much the same as your flexible grid. Even though you specified only a single column, the adaptive grid elements fill the phone width with two columns.
The new behavior becomes more noticeable when you rotate the phone device. Rotate the device or simulator, and you’ll see the columns fill the display’s width instead of limiting it to two columns. SwiftUI chooses a size that allows equal-width columns to maximize the number of columns for the enclosing view. For most current iPhones, that’s four columns.
Change your device to the 11 inch iPad simulator. Run the app, and you’ll see the grid again adapts to use the extra space and shows four columns for the grid.
The three types of columns each meet a different use case. The fixed and flexible types allow you to specify a column you want to appear, either limited to a specific size or range of sizes. The adaptive column fills the available space with as many items as will fit in the view. When you need more flexibility, you can mix and combine the different column types in any way necessary for your app.
You’ll need to choose the layout depending the data you’re showing. For this app, you want to show all the awards in a compact space and will use the adaptive type.
In Chapter 14: Lists, you saw that you group data into sections to help users understand what they’re viewing. Grids offer this same ability and you’ll add that in the next section.
Using Sections in Grids
To help the user understand what award they have yet to receive, you’ll divide the awarded and not-awarded items into separate sections. Add two new computed properties below the awardArray property:
var activeAwards: [AwardInformation] {
awardArray.filter { $0.awarded }
}
var inactiveAwards: [AwardInformation] {
awardArray.filter { !$0.awarded }
}
These filter the array of all awards to awarded awards and not awarded awards, respectively. Since each section will display the same information, you’ll extract the grid into a separate view. At the top of the file after import SwiftUI, add the following code:
struct AwardGrid: View {
// 1
var title: String
var awards: [AwardInformation]
var body: some View {
// 2
Section(
// 3
header: Text(title)
.frame(maxWidth: .infinity)
.font(.title)
.foregroundColor(.white)
.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: 10)
)
) {
// 4
ForEach(awards) { award in
NavigationLink(value: award) {
AwardCardView(award: award)
.foregroundColor(.black)
.aspectRatio(0.67, contentMode: .fit)
}
}
}
}
}
You’ve extracted the view from the grid. Here are the changes you’ve made:
- These properties contain the section’s title and an array of awards to show in this grid.
- The
Sectionview creates a group within the grid whose contents will be the closure of the view. - You pass a view as the
headerproperty. SwiftUI displays this view at the top of the grid. You could also specify afooterview in the same way. In this case, you show the title passed to the view. - The closure consists of the loop to show the passed awards you’ve used throughout this chapter. This closure is the same as the view you had at the end of the previous section.
Now you can use the extracted view in the grid. Change the LazyVGrid to:
LazyVGrid(columns: awardColumns) {
AwardGrid(
title: "Awarded",
awards: activeAwards
)
AwardGrid(
title: "Not Awarded",
awards: inactiveAwards
)
}
You’ll see you now have two sections, the first showing awards the user has received and the second those the user has not yet received.
Key Points
- SwiftUI provides two types of lazy loaded grids:
LazyVGrid, which grows vertically andLazyHGrid,which grows horizontally. - You define columns for a
LazyVGridand rows for aLazyHGrid. AGridItemdescribes the layout for both types of grids. - A
fixedgrid item lets you specify an exact size for a column or row. - A
flexiblegrid item lets you specify a range of sizes while still defining the number of columns. - An
adaptivegrid item can adapt to fill the available space in a view using provided size limits. - You can mix different types of grid items in the same row or column.
Where to Go From Here?
To see more about what creating grids required in the initial release of SwiftUI, see Chapter 20: Complex Interfaces in the second edition of this book.
These Apple’s 2020 WWDC videos offer Apple’s introduction to grids in SwiftUI:
- Stacks, Grids, and Outlines in SwiftUI, https://apple.co/3LgFNE9
- What’s new in SwiftUI, https://apple.co/3mQ6VQI
To look beyond linear displays, check out:
- Creating a Mind-Map UI in SwiftUI tutorial, https://www.kodeco.com/7705231-creating-a-mind-map-ui-in-swiftui.