The UICollectionView is a wildly popular control in UIKit, developers have been hungry for a SwiftUI counterpart since it was introduced at WWDC19. Up until now we've been able to use combinations of VStack and HStack to achieve similar results, but known deep down that there's a better way!

SwiftUI 2 introduces LazyHGrid and LazyVGrid to bridge the gap between stacks in SwiftUI and UICollectionView in UIKit, these grids will dynamically resize your SwiftUI views based on the layout parameters you provide.

You may be wondering why they're both marked with Lazy, it's because of the support for lazy loading, meaning they won't load the contents until they are needed, you may be familiar the lazy keyword already in swift. SwiftUI 2 also introduces LazyVStack and LazyHStack based on the same principle.

At the time of writing this you'll need the latest beta of Xcode 12 to run this code.


GridItem

to work with the new grid types in SwiftUI you'll first need to understand GridItem. One or more instances of GridItem supply the layout parameters for grids in SwiftUI and can either represent a row or a column. The grid item constructor looks like this , init(GridItem.Size, spacing: CGFloat?, alignment: Alignment?) where

  • GridItem.Size is an enum that can be .adaptive, .fixed or flexible
  • Spacing represents the distance between each view in the grid
  • alignment is the layout alignment on either axes

To quickly elaborate on GridItem.Size

You can use multiple instances to achieve more complex layouts, for example if you want three columns in a LazyVGrid you can provide an array of three .flexible() grid items to the constructor. This will hopefully become clearer when we see some examples.


The LazyVGrid and LazyHGrid both rely on an array of GridItems to dictate their layout, the former using  them for columns and the latter using them for rows. Let's take a look at this in practice starting with the LazyVGrid.

    var columns: [GridItem] =
        [.init(.adaptive(minimum: 50, maximum: 100))]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 2) {
                ForEach((0...100), id: \.self) { index in
                    RoundedRectangle(cornerRadius: 5)
                        .foregroundColor(.pink)
                        .frame(height: 50)
                    RoundedRectangle(cornerRadius: 5)
                        .foregroundColor(.blue)
                        .frame(height: 50)
                }
            }.font(.body)
        }
    }

Notice we're using the GridItem.Size.adaptive, this will fill an entire row with  Views using the minimum and maximum values for sizing. Below you can see how the LazyVGrid looks when used with different combinations of GridItem.Size.

Let's see what happens when we apply the same GridItems to LazyHGrid.

    var rows: [GridItem] =
        [.init(.adaptive(minimum: 50, maximum: 100))]
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: rows, spacing: 2) {
                ForEach((0...100), id: \.self) { index in
                    RoundedRectangle(cornerRadius: 5)
                        .foregroundColor(.pink)
                        .frame(width: 50)
                    RoundedRectangle(cornerRadius: 5)
                        .foregroundColor(.blue)
                        .frame(width: 50)
                }
            }.font(.body)
        }
    }

How has this code changed?

  • Renamed columns to rows
  • Changed LazyVGrid to LazyHGrid
  • Changed first parameter in grid initialiser from columns to rows
  • Gave ScrollView a layout direction of .horizontal

Let's see how this looks.

With those minor changes we've changed the layout from a vertically scrolled grid to a horizontally scrolled grid. Play around with different combinations of GridItem layouts in your own code and see the results! It's a lot of fun.

Thanks for reading ✌️