SwiftUI Prototype Tutorial 3 of 5: Categories List

Devin Jameson

In this installment of our SwiftUI tutorial series, we’ll build out our Categories view.

Posts in this series:

Here’s what our prototype will look like at the end of this section.

An iPhone 11 Pro max running our prototype. On the prototype is a bottom tab bar with two options: Categories and Profile. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The categories tab is blue and the profile tab is black. Above the bottom tab bar, there are six identical images of a foggy New York City taken from above a road, looking down it. There are deep green trees on either side of the road. The images are rectangular, with more height than width, and they are arranged in two columns and three rows. The image has rounded corners and, overlayed in the bottom right corner is the text "Business" in white.

Now let’s get down to business.

:)

To get started, we’ll create a view that contains each row of categories and call it CategoryRow. Inside this view, we’ll include two instances of CategoryCard within an HStack.

struct CategoryRow: View {
    var body: some View {
        HStack { // Like ZStack, but positions views horizontally
            CategoryCard()
            CategoryCard()
        }
    }
}

Now we can use our CategoryRow view in our Categories view. Let’s add four instances of it within a VStack.

struct Categories: View {
    var body: some View {
        VStack { // Like HStack, but positions views vertically
            CategoryRow()
            CategoryRow()
            CategoryRow()
            CategoryRow()
        }
    }
}

Now we should have four rows of Category Cards visible.

An iPhone 11 Pro max running our prototype. On the prototype is a bottom tab bar with two options: Categories and Profile. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The categories tab is blue and the profile tab is black. Above the bottom tab bar, there are six identical images of a foggy New York City taken from above a road, looking down it. There are deep green trees on either side of the road. The images are square and they are arranged in two columns and three rows. The image has rounded corners and, overlayed in the bottom right corner is the text "Business" in white. When compared to the previous image, there is less white space above the set of images.

We want to be able to scroll through this list, so let’s put our VStack within a ScrollView. While we’re at it, we can add some default padding to our VStack to give our content some room to breathe.

struct Categories: View {
    var body: some View {
        ScrollView {
            VStack {
                CategoryRow()
                CategoryRow()
                CategoryRow()
                CategoryRow()
            }
            .padding()
        }
    }
}

We should now have a scrollable list of Category Cards.

An iPhone 11 Pro max running our prototype. On the prototype is a bottom tab bar with two options: Categories and Profile. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The categories tab is blue and the profile tab is black. Above the bottom tab bar, there are six identical images of a foggy New York City taken from above a road, looking down it. There are deep green trees on either side of the road. The images are arranged in two columns and three rows. The image has rounded corners and, overlayed in the bottom right corner is the text "Business" in white. When compared to the previous image, in this image there is more white space above the set of images.

This is looking great already, but we want each Category Card to have a rectangular shape. You might think we could just change the hardcoded height and width arguments in the frame method on our Image. However, this solution wouldn’t account for various device sizes.

In this case, we want our width and height values to be dynamic, so we’ll use GeometryReader. GeometryReader is a container view which provides access to its own size and coordinate space. We’ll use a fraction of the width component of this size to set the width and height of each Category Card.

We also we need to instantiate GeometryReader outside our ScrollView or it won’t behave properly.

Let’s start by doing that.

struct Categories: View {
    var body: some View {
        GeometryReader { geometry in // Add GeometryReader
            ScrollView {
                VStack {
                    CategoryRow()
                    CategoryRow()
                    CategoryRow()
                }
                .padding()
            }
        }
    }
}

Great! Now we have access to our screen size in our Categories view via geometry. Let’s pass geometry down to our CategoryRow view.

struct Categories: View {
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack {
                    CategoryRow(geometry: geometry) // Pass geometry to CategoryRow
                    CategoryRow(geometry: geometry)
                    CategoryRow(geometry: geometry)
                    CategoryRow(geometry: geometry)
                }
                .padding()
            }
        }
    }
}

Since CategoryRow doesn’t take any arguments yet, we’ll have to add a parameter geometry of type GeometryProxy.

struct CategoryRow: View {
    let geometry: GeometryProxy // Add geometry parameter to CategoryRow

    var body: some View {
        HStack {
            CategoryCard()
            CategoryCard()
        }
    }
}

Now we have access to geometry in CategoryRow. We’ll repeat this process to pass geometry down to our CategoryCard.

struct CategoryRow: View {
    let geometry: GeometryProxy

    var body: some View {
        HStack {
            CategoryCard(geometry: geometry) // Pass geometry to CategoryCard
            CategoryCard(geometry: geometry)
        }
    }
}
struct CategoryCard: View {
    let geometry: GeometryProxy // Add geometry parameter to CategoryCard

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            Image("business")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 200, height: 200)

Now, we can finally use geometry in place of our static values for our frame width and height.

Image("business")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(
        width: geometry.size.width * 0.45, // Set width and height to a fraction of view width
        geometry.size.width * 0.55)

An iPhone 11 Pro max running our prototype. On the prototype is a bottom tab bar with two options: Categories and Profile. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The categories tab is blue and the profile tab is black. Above the bottom tab bar, there are six identical images of a foggy New York City taken from above a road, looking down it. There are deep green trees on either side of the road. The images are rectangular, with more height than width, and they are arranged in two columns and three rows. The image has rounded corners and, overlayed in the bottom right corner is the text "Business" in white.

Nice! Now we have a list of Category Cards. See you for Part Four, in which we’ll make our categories list dynamic and add some navigation.