Waldo joins Tricentis, expanding mobile testing for higher-quality mobile apps – Learn more
App Development

A Native SwiftUI Collection View: LazyVGrid and LazyHGrid

Juan Reyes
Juan Reyes
A Native SwiftUI Collection View: LazyVGrid and LazyHGrid
June 17, 2021
7
min read

In 2019, Apple introduced a change to the way developers could design apps for the Apple ecosystem. The launch of SwiftUI began a long process of change that arguably was well overdue. It has dramatically benefited developers’ productivity and engagement with the platform. And now it’s much simpler to create beautiful UI for our users.

However, during that first launch with Xcode 11, something crucial was missing in the development toolset. There was no out-of-the-box collection view to display views. That omission has thankfully been corrected since the Apple worldwide developers’ conference of 2020. Nevertheless, developers had to rely on custom solutions to display their views in their new SwiftUI-powered apps.

In this article, we’ll explore the collection views available to us in SwiftUI: LazyVGrid and LazyHGrid. You’ll work on a simple implementation to better grasp the concepts introduced in this article. You’ll also customize the appearance of these elements. And finally, you’ll perform some testing to ensure the code stays functional.

A Quick Word on SwiftUI Workflow

Before you jump into any code, let’s briefly explore what a new SwiftUI project is in Xcode.

As soon as you produce a new SwiftUI project, you’ll notice that your template project includes a ContentView.swift file and an <APP_NAME>App.swift file. These files are the essential SwiftUI project. They’re relatively simple to understand because all SwiftUI view files have similar structures.

  • There’s a ContentView, containing a “body” variable that defines the design of the view.
  • Also, there’s a PreviewView to display the code in the previewer as it would on the actual device.

To modify your view, you can add your code inside the body variable. Notice that there’s already a TextView with our familiar “Hello World!” in it.

Now, you can work on that body variable to create your example.

But what if you have limited experience working with Swift and still feel a bit lost? In that case, please consider diving into this extensive documentation here.

SwiftUI Grids

As Apple describes it, a grid is a collection view that enables the layout of elements in a particular format for navigation and interaction. This description covers ScrollViews, Stacks, Lists, and Grids.

Lazy grids—and lazy stacks, for that matter—are specialized collection views that can load their elements when they’re needed for display. Therefore, LazyVStacks, LazyHStacks, LazyVGrids, and LazyHGrids are much less resource intensive than standard Stacks. And that makes them a lot more flexible for the developer.

Therefore, LazyVStacks, LazyHStacks, LazyVGrids, and LazyHGrids are much less resource intensive than standard Stacks.

That being said, throwing a Lazy view everywhere isn’t always the best course of action. After all, there’s a tradeoff in the complexity of implementing these elements compared with their more straightforward siblings. So, consider the suitable application depending on the requirements.

LazyVGrid

A LazyVGrid is a container view that displays its views in a vertical arrangement for interaction. It loads the items on demand, depending on how the user consumes them.

An example of a LazyVGrid would look like the following.

 
 
import SwiftUI
struct ContentView: View {
    let items = 1...50
    let config = [
        GridItem(.fixed(80))
    ]
    var body: some View {
        ScrollView {
            LazyVGrid(columns: config, spacing: 20) {
                ForEach(items, id: \.self) { item in
                    Image(systemName: "\(item).square.fill")
                                            .font(.largeTitle)
                }
            }
        }
        .frame(maxHeight: 350)
    }
}

As you can see, this code is relatively straightforward. We have two variables defined: an array of 50 items and an array of GridItems that determine the configuration of the grid. Don’t worry if you don’t understand the code yet. We’ll dive into that in a minute.

Inside the body, the LazyVGrid is embedded in a ScrollView. This lets the user interact with the grid and scroll them to display. The LazyVGrid constructor has two parameters you must pass: the column structure and the padding between the items.

Inside the LazyVGrid, you have the structure that processes the views from a collection to display. In this case, it’s a ForEach.

Finally, you have an Image view that creates the icons displayed in the list.

Here’s the resulting view:

screen of column grid

Notice that it creates a simple, vertically scrollable list with one column.

These items will be initialized as you scroll. And that saves the app the processing time and memory needed to have all these items initialized on creation.

Let’s keep moving.

LazyHGrid

Like the LazyVGrid, a LazyHGrid is a container view that displays its views horizontally and loads its items when needed.

Now, let’s see how a LazyHGrid would look in code.

 
 
import SwiftUI
struct ContentView: View {
    let items = 1...50
    let config = [
        GridItem(.fixed(80))
    ]
    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: config, alignment: .center) {
                ForEach(items, id: \.self) { item in
                    Image(systemName: "\(item).square.fill")
                                            .font(.largeTitle)
                }
            }
        }
        .frame(maxHeight: 350)
    }
}

That doesn’t look much different, does it?

That’s right. A LazyHGrid follows basically the same structure as a LazyVGrid, with some minor differences.

For one, the ScrollView needs to be defined as horizontal scrolling. Additionally, the LazyHGrid contains different constructor parameters. In this case, they are rows and alignment. That’s pretty self-explanatory.

Those changes would yield the following view.

screen of horisontal grid

Notice that now the items are centered vertically and scroll horizontally. Try it out!

All right! That’s pretty neat.

Now, let’s dive a little deeper into the nitty gritty of how the GridItem sets up the layout of the grids.

GridItem

A GridItem is an additional item that describes the layout of the LazyVGrid and LazyHGrid. It’s an interface of sorts so that your grids can correctly display the items.

This element is pretty helpful because LazyVGrid and LazyHGrid are supposed to handle any view. However, to achieve that level of flexibility, you’ll need to provide a bit more information about your objects.

A GridItem is an additional item that describes the layout of the LazyVGrid and LazyHGrid.

If you go back to your first code example, you can see that the GridItem config instance is defined as fixed. This tells the LazyVGrid to display the items with a fixed column size that spans over the whole width of the view. Moreover, since there’s only one instance of the GridItem in the config variable, the LazyVGrid will display only one column. Therefore, adding another GridItem will result in a second column, and so on.

Now, here are the possible configurations for the GridItem according to Apple’s documentation:

  • Fixed: This is a single item with the specified fixed size.
  • Flexible: This is a single flexible item. The size of this item is the size of the grid with spacing, and inflexible items removed, divided by the number of flexible items clamped to the provided bounds.
  • Adaptive: Multiple items in the space of a single flexible item. This size case places one or more items into the space assigned to a single “flexible” item, using the provided bounds and spacing to decide exactly how many items fit. This approach prefers to insert as many items of the minimum size as possible but lets them increase to the maximum size.

To demonstrate how this works, let’s talk about how you can tweak your grid appearance.

Grid Appearance

So, what would happen if you used, say, a combination of these?

 
 
import SwiftUI
struct ContentView: View {
    let items = 1...50
    let config = [
        GridItem(.fixed(80)),
        GridItem(.flexible(minimum: 50))
    ]
    var body: some View {
        ScrollView {
            LazyVGrid(columns: config, spacing: 20) {
                ForEach(items, id: \.self) { item in
                    Image(systemName: "\(item).square.fill")
                                            .font(.largeTitle)
                }
            }
        }
        .frame(maxHeight: 350)
    }
}

As you can see in the code above, I’ve added a flexible GridItem to the configuration. This setup results in the following:

Screen of two grid columns

Notice that the first column has a set size, and the second column spans to occupy the rest of the space. Also, you can set a maximum size to the flexible configuration.

All right! How about the adaptive configuration?

Let’s see how you can use adaptive to let the LazyVGrid position the items as they fit horizontally and vertically.

 
 
import SwiftUI
struct ContentView: View {
    let items = 1...50
    let config = [
        GridItem(.adaptive(minimum: 40))
    ]
    var body: some View {
        ScrollView {
            LazyVGrid(columns: config, spacing: 20) {
                ForEach(items, id: \.self) { item in
                    Image(systemName: "\(item).square.fill")
                                            .font(.largeTitle)
                }
            }
        }
        .frame(maxHeight: 350)
    }
}

In this case, you need to set only one GridItem configuration.

Screen of grid

As you can see, this configuration accommodates as many columns as there could fit horizontally and then spans the rest of the content vertically. That’s pretty useful for a more dynamic and responsive layout, such as photo galleries or resizable views.

Pretty great, right?

Test Your Grids

Finally, let’s create a simple testing routine to ensure the quality of your work.

To do that, you’re going to move to the Test_iOS.swift file and work on the testExample method. Once you’re there, you can add the following code to test that the grid displays correctly and responds as expected.

 
 
func testExample() throws {
    // UI tests must launch the application that they test.
    let app = XCUIApplication()
    app.launch()
    // Get a handle for the scrollView
    let scrollview = app.scrollViews.element(boundBy: 0)
    guard var lastCell = scrollview.images.allElementsBoundByIndex.last else { return }
    //Add in a count, so that the loop can escape if it's scrolled too many times
    let MAX_SCROLLS = 10
    var count = 0
    while lastCell.isHittable == false && count < MAX_SCROLLS {
        app.swipeUp()
        count += 1
        lastCell = scrollview.images.allElementsBoundByIndex.last!
    }
    XCTAssertTrue(lastCell.label == "50.square.fill")
    // Use recording to get started writing UI tests.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

Wonderful.

This is a very simplistic test scenario. Ideally, you should be more thorough with your tests. For that purpose, I recommend considering Waldo's extensive toolset for UI testing. No coding is required, which is excellent for non-developers. There’s a free trial and a helpful blog.

Wrapping up

Navigating the extensive toolset available in Apple’s ecosystem can be tiring and overwhelming at times. Sometimes, the solution you’re looking for isn’t available, or there just isn’t enough documentation at the moment. That was certainly the case in 2019. Nevertheless, SwiftUI continues to mature steadily, and more tools become part of your set. This lets you bring more elegant and functional solutions to your users.

Automated E2E tests for your mobile app

Creating tests in Waldo is as easy as using your app!
Learn more about our Automate product, or try our live testing tool Sessions today.

Reproduce, capture, and share bugs fast!

Waldo Sessions helps mobile teams reproduce bugs, while compiling detailed bug reports in real time.