SwiftUI Prototype Tutorial 4 of 5: Dynamic Categories & Navigation

In this installment of our SwiftUI tutorial series, we’ll make our categories list dynamic by displaying a unique image and title on each card. Then we’ll add navigation to our Categories list.

Posts in this series:

Here’s a look at what we’ll create in this section.

The prototype we'll be creating, previewed on an iPhone 11 Pro Max. A cursor is clicking on the screen to navigate to different views. The prototype has two tabs shown at the bottom of the screen: Categories and Profile. The categories tab is blue and the profile tab is black. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The Categories view shows eight clickable categories, each of which is represented by an image associated with that category and an overlayed label with the category name in white text. When the cursor clicks on the Business category, we see a new screen. On the top left of this screen are the headline "Business" and, above that, a blue back button with the text "Categories". The bottom tab bar is still on this screen.

Since we want each CategoryCard to receive a different name, we’ll start by creating a category name parameter within our CategoryCard view.

struct CategoryCard: View {
    let geometry: GeometryProxy
    let categoryName: String

Now we can use our categoryName in place of the string “Business” within our CategoryCard.

Text(categoryName) // Use categoryName in place of our static string
    .font(.headline)
    .foregroundColor(Color.white)
    .padding(12)

We’ll see an error in our CategoryRow view because we haven’t supplied an argument to the categoryName parameter. Let’s fix that. For the first CategoryCard, we’ll set categoryName equal to categoryNameLeft. For the second, we’ll set categoryName equal to categoryNameRight. Later, we’ll pass in values for categoryCardLeft and categoryCardRight from our Categories view.

HStack {
    CategoryCard(geometry: geometry, categoryName: categoryNameLeft)
    CategoryCard(geometry: geometry, categoryName: categoryNameRight)
}

Now we’ll get the error: Use of unresolved identifier on both CategoryCards. To fix this, we’ll add parameters for categoryCardLeft and categoryCardRight to CategoryRow.

struct CategoryRow: View {
    let geometry: GeometryProxy
    let categoryNameLeft: String // Add parameter for categoryNameLeft
    let categoryNameRight: String // Add parameter for categoryNameRight

    var body: some View {

We have one more error to deal with because we haven’t provided arguments for categoryNameLeft and categoryNameRight when we use CategoryRow. Let’s fix that, too.

ScrollView {
    VStack {
        CategoryRow(geometry: geometry, categoryNameLeft: "Business", categoryNameRight: "Science")
        CategoryRow(geometry: geometry, categoryNameLeft: "Sports", categoryNameRight: "Opinion")
        CategoryRow(geometry: geometry, categoryNameLeft: "Finance", categoryNameRight: "Politics")
        CategoryRow(geometry: geometry, categoryNameLeft: "Health", categoryNameRight: "Arts")
    }

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 eight clickable categories, each of which is represented by an image 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. Overlayed on each image is a label with the category name in white text. The images are rectangular, with more height than width, and they are arranged in two columns and three rows. The images have rounded corners.

Looking good! Now we want to show a different image for each category. Download the rest of the images here and add them to your Assets using the process from Part Two.

Now that we have access to our images, all we have to do is lowercase our categoryName and use that value, instead of the string “business”, to reference our category image.

var body: some View {
    ZStack(alignment: .bottomTrailing) {
        Image(categoryName.lowercased()) // Replace our static image
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(width: geometry.size.width * 0.45, height: geometry.size.width * 0.55)
        Text(categoryName)

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 eight clickable categories, each of which is represented by an image associated with that category and an overlayed label with the category name in white text. The images are rectangular, with more height than width, and they are arranged in two columns and three rows. The images have rounded corners.

Our Categories view is almost complete! All that’s left is to add some navigation.

In our Categories view, outside GeometryReader, we’ll add a NavigationView.

struct Categories: View {
    var body: some View {
        NavigationView { // Add NavigationView
            GeometryReader { geometry in
                ScrollView {

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 eight clickable categories, each of which is represented by an image associated with that category and an overlayed label with the category name in white text. The images are rectangular, with more height than width, and they are arranged in two columns and three rows. The images have rounded corners. Compared to the previous image, there is more white space above the list of categories.

You’ll notice some white space appear at the top of our Categories view. This is where our navigation bar title would go. However, we don’t want a title on this screen given that the title is in our TabView. We can hide this space by using the .navigationBarTitle() and .navigationBarHidden() methods.

NavigationView {
    GeometryReader { geometry in
        ScrollView {
            VStack {
                CategoryRow(geometry: geometry, categoryCardLeft: "Business", categoryCardRight: "Science")
                CategoryRow(geometry: geometry, categoryCardLeft: "Sports", categoryCardRight: "Opinion")
                CategoryRow(geometry: geometry, categoryCardLeft: "Finance", categoryCardRight: "Politics")
                CategoryRow(geometry: geometry, categoryCardLeft: "Health", categoryCardRight: "Arts")
            }
            .padding()
        }
        .navigationBarTitle("Categories") // Set the navigation bar title
        .navigationBarHidden(true) // Hide the navigation bar title
    }
}

We still have to provide a navigation bar title, but now it will be hidden on this screen. Note that we must use the .navigationBarTitle() and .navigationBarHidden() methods within NavigationView.

Next, we’ll wrap each of our Category Cards in a NavigationLink. We have to provide a destination for each NavigationLink, so for now we’ll just use a Text view with an empty string.

var body: some View {
        HStack {
            NavigationLink(destination: Text("")) {
                CategoryCard(geometry: geometry, categoryName: categoryNameLeft)
            }
            NavigationLink(destination: Text("")) {
                CategoryCard(geometry: geometry, categoryName: categoryNameRight)
            }
        }
    }

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 eight clickable categories, each of which is represented by a blue rounded rectangle and an overlayed label with the category name in white text. The rectangles have more height than width, and they are arranged in two columns and three rows.

It appears the Blue Man Group has invaded our device. To fix our category card styling, we can change the buttonStyle of our navigation links.

HStack {
    NavigationLink(destination: Text("")) {
        CategoryCard(geometry: geometry, categoryName: categoryCardLeft)
    }
    NavigationLink(destination: Text("")) {
        CategoryCard(geometry: geometry, categoryName: categoryCardRigh
    }
}
.buttonStyle(PlainButtonStyle()) // Change button style of navigation links

We can now click on each Category and be brought to a blank screen. As a final step, we’ll swap out this blank screen and instead show the name of the clicked category.

Our prototype running on an iPhone 11 Pro Max. A cursor is clicking on the screen to navigate to different views. The prototype has two tabs shown at the bottom of the screen: Categories and Profile. The categories tab is blue and the profile tab is black. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The Categories view shows eight clickable categories, each of which is represented by an image associated with that category and an overlayed label with the category name in white text. When the cursor clicks on a category, we see a new screen. On the top left of this screen is a blue back button with the text "Categories". The bottom tab bar is still on this screen. The cursor then clicks on the back button to navigate to the prior screen.

First, we’ll create a view for our Category screen. We don’t want any content on this page besides the navigation bar title, so we’ll just add a VStack with an empty Text view.

struct Category: View {
    var body: some View {
        VStack {
            Text("")
        }
    }
}

Now let’s supply our new Category view to our NavigationLink destination parameters.

HStack {
    NavigationLink(destination: Category()) { // Change the destination to Category()
        CategoryCard(geometry: geometry, categoryName: categoryNameLeft)
    }
    NavigationLink(destination: Category()) { // Change the destination to Category()
        CategoryCard(geometry: geometry, categoryName: categoryNameRight)
    }
}

To display the category name in the Category view, we’ll just pass the categoryName through to the Category View and supply it to the navigationBarTitle method.

HStack {
    NavigationLink(destination: Category(categoryName: categoryNameLeft)) { // Pass the categoryName to Category()
        CategoryCard(geometry: geometry, categoryName: categoryNameLeft)
    }
    NavigationLink(destination: Category(categoryName: categoryNameRight)) { // Pass the categoryName to Category()
        CategoryCard(geometry: geometry, categoryName: categoryNameRight)
    }
}
struct Category: View {
    let categoryName: String // Add a parameter for the category name

    var body: some View {
        VStack {
            Text("")
        }
        .navigationBarTitle(categoryName) // Use the category name
    }
}

The prototype we'll be creating, previewed on an iPhone 11 Pro Max. A cursor is clicking on the screen to navigate to different views. The prototype has two tabs shown at the bottom of the screen: Categories and Profile. The categories tab is blue and the profile tab is black. Above the categories tab label is a globe icon, and above the profile tab label is a person icon. The Categories view shows eight clickable categories, each of which is represented by an image associated with that category and an overlayed label with the category name in white text. When the cursor clicks on the Business category, we see a new screen. On the top left of this screen are the headline "Business" and, above that, a blue back button with the text "Categories". The bottom tab bar is still on this screen.

That’s it for this section! See you for Part Five, in which we’ll create our Profile view.