15.
Grids
Written by Bill Morefield
Several features didn’t make the initial release of SwiftUI. One of the most lamented ones was the lack of a native collection view. This view is so useful that earlier editions of this book included a chapter that walked through creating a reusable grid view.
The second release of SwiftUI made that chapter obsolete with the addition of 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 could create more complex layouts, even with these limitations. You just had to do the work yourself.
Open the starter project and run the app. You’ll see the buttons on the welcome screen now use a new vertical arrangement in a vertical stack.
With this new shape, the buttons would work better in a grid. First you’ll create a grid layout as you would in the inital version of SwiftUI. Open WelcomeView.swift and change the closure of the ScrollView to:
// 1
VStack {
// 2
HStack {
FlightStatusButton(flightInfo: flightInfo)
Spacer()
SearchFlightsButton(flightInfo: flightInfo)
}
// 3
HStack {
AwardsButton()
// 4
LastViewedButton(
flightInfo: flightInfo,
appEnvironment: appEnvironment,
showNextFlight: $showNextFlight
)
}
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 an HStack inside a VStack.
- 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. - This
HStackmakes the second row of the grid. - If the user hasn’t viewed a flight, LastViewedButton will be a
Spacerto keep the number of elements in the two rows identical.
Run the app, and you’ll see the grid.
In the initial release of SwiftUI, this technique was the only way to build a grid. This book’s previous editions included a chapter on creating a generic reusable grid that you can consult if you’d like to see more on this technique. With the second release of SwiftUI, there’s now a native and more flexible option 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 control builds on top of 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 WelcomeView.swift to:
// 1
LazyVGrid(
// 2
columns: [
// 3
GridItem(.fixed(160)),
GridItem(.fixed(160))
// 4
], spacing: 15
) {
FlightStatusButton(flightInfo: flightInfo)
SearchFlightsButton(flightInfo: flightInfo)
AwardsButton()
LastViewedButton(
flightInfo: flightInfo,
appEnvironment: appEnvironment,
showNextFlight: $showNextFlight
)
}.font(.title)
.foregroundColor(.white)
.padding()
Notice this doesn’t look that different from the initial LazyVStack. That’s the beauty of the 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
GridItemsto describe the grid. Here you use the simplest type ofGridItem, a fixed column you set 160 points wide. The sub-views for each column are 155 points wide, leaving a five-point space as a margin until the next column. - 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.
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: This 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. You’ll see the grid looks similar to that from the last section, but with smaller columns since you set them to 160 points.
Building flexible grids
A static grid works for many cases, but you have 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 also setting the number of rows or columns in the grid.
Open AwardsView.swift. The app supports awards the user receives for completing tasks. This view displays the user’s current awards along with those the user hasn’t received yet. Right now, the list displays as a vertical stack, much as the initial welcome view did.
You’ll change it to use a grid. Having the column information inside the view often clutters the 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 contents of the ScrollView to:
LazyVGrid(columns: awardColumns) {
ForEach(awardArray, id: \.self) { award in
NavigationLink(destination: AwardDetails(award: award)) {
AwardCardView(award: award)
.foregroundColor(.black)
.frame(width: 150, height: 220)
}
}
}
Again you replaced the previous LazyVStack view with a LazyVGrid and passed it the awardColumns property you previously added to define the grid columns. Run the app and tap Your Awards. You’ll see the awards now show in a two-column grid.
Interacting between views and columns
It’s worth spending a moment 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.
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: 150, maximum: 170)),
GridItem(.flexible(minimum: 150, maximum: 170))]
}
Note: The canvas will not always update 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 170 points. The card still has a width of 190 points. Run the app and view the awards to see the effect.
As you might expect, your grid becomes a bit cramped. Your card’s frame sets the width at 190 points, but the column can only extend to 170 points because of the maximum: 170 constraint. As a result, the contents of the grid cells can overlap or clip.
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 method creates a side effect that you no longer set the dimensions. You can use a different modifier to keep the shape of the view.
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.
One 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.
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, but that would crowd in 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, an 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 now fill the width of the display 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, now showing five columns for the grid.
The three types of columns each meet a different 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, respectively. The adaptive type fills the available space with as many items as will fit.
When you need more flexibility, you can mix and combine the different column types in any way necessary for your app.
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.
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 only 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)
.font(.title)
.foregroundColor(.white)
) {
// 4
ForEach(awards, id: \.self) { award in
NavigationLink(
destination: AwardDetails(award: 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. This closure is the same as the view you were previously using inside the grid.
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 completed.
Key points
- SwiftUI provides two types of grids:
LazyVGrid, which grows vertically andLazyHGrid,which grows horizontally. - You define columns for a
LazyVGridand rows for aLazyHGrid. AGridItemdescribes the layout of 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 provides 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 creating grids required in the initial release of SwiftUI, see Chapter 15: Complex Interfaces in the second edition of this book.
Apple’s 2020 WWDC videos - Stacks, Grids, and Outlines in SwiftUI, https://developer.apple.com/videos/play/wwdc2020/10031/ and What’s new in SwiftUI, https://developer.apple.com/videos/play/wwdc2020/10041 offer Apple’s introduction to grids in SwiftUI.
To look beyond linear displays, check out “Creating a Mind-Map UI in SwiftUI” https://www.raywenderlich.com/7705231-creating-a-mind-map-ui-in-swiftui.