Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

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.

Initial Awards View
Initial Awards 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.

  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. Note that you manually reference the elements of the awardArray array.
  3. This HStack makes the second row of the grid.

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

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

  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.
  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.
  5. Thanks to the grid, you can use ForEach to iterate over the awardArray array as it separates the elements of the grid in the closure to the LazyVGrid from the layout defined using the columns parameter to the LazyVGrid.
  6. Each item in the grid will be a NavigationLink, which sets the value to the award passed to the closure. It displays the AwardCardView for the award as the view.
  7. 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 rows and pass in descriptions of the grid’s rows. The GridItem works 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.

Awards Shown in a Grid
Awards Shown in a Grid

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.

Flexible grid screen
Flexible grid screen

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:

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. 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.

Award grid still clipped
Award grid still clipped

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.

Award grid screen with size by aspect ratio
Award grid screen with size by 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.

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, 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.

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 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.

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 and shows four columns for the grid.

Award grid iPad
Award grid iPad

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:

  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 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.

Award grid with sections
Award grid with sections

Key Points

  • SwiftUI provides two types of lazy loaded 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 for 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 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:

To look beyond linear displays, check out:

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.