Chapters

Hide chapters

SwiftUI by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

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.

Mountain Airport app initial screen
Mountain Airport app initial screen

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.

  1. Using an initial VStack creates the overall vertical layout of the grid.
  2. This HStack builds the first row of the grid. It contains two of the button views separated by a Spacer.
  3. This HStack makes the second row of the grid.
  4. If the user hasn’t viewed a flight, LastViewedButton will be a Spacer to keep the number of elements in the two rows identical.

Run the app, and you’ll see the grid.

Manual Grid
Manual 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:

  1. The LazyVGrid builds a set of rows that extend vertically. The corresponding LazyHGrid creates a list of rows that extends horizontally.
  2. The new parameter for a vertical grid is columns. You pass it an array of GridItem elements that describes the columns.
  3. The array consists of a set of GridItems to describe the grid. Here you use the simplest type of GridItem, 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.
  4. You also pass the optional spacing parameter. 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 rows and pass in descriptions of the grid’s rows. The GridItem works 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.

Welcome Grid screen
Welcome Grid screen

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.

Awards screen
Awards screen

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.

Flexible grid screen
Flexible grid screen

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:

Flexible grid with larger cards
Flexible grid with larger cards

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.

Award grip clipped
Award grip clipped

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.

Award grid screen
Award grid screen

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.

Award grid in horizontal mode with 2 columns
Award grid in horizontal mode with 2 columns

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.

Adaptive award grid in vertical
Adaptive award grid in vertical

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.

Adaptive award grid in horizontal
Adaptive award grid in horizontal

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.

Award grid iPad
Award grid iPad

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:

  1. These properties contain the section’s title and an array of awards to show in this grid.
  2. The Section view creates a group within the grid whose contents will be the closure of the view.
  3. You pass a view as the header property. SwiftUI displays this view at the top of the grid. You could also specify a footer view in the same way. In this case, you show the title passed to the view.
  4. 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.

Award grid finished screen
Award grid finished screen

Key points

  • SwiftUI provides two types of grids: LazyVGrid, which grows vertically and LazyHGrid, which grows horizontally.
  • You define columns for a LazyVGrid and rows for a LazyHGrid. A GridItem describes the layout of both types of grids.
  • A fixed grid item lets you specify an exact size for a column or row.
  • A flexible grid item lets you specify a range of sizes while still defining the number of columns.
  • An adaptive grid 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.