Creating Lists in SwiftUI

Creating the category cells

First, we want to create the cells showing the category names inside images with rounded corners. We can use our ContentView for this. But instead of using the default Text view, we delete it and insert an Image view instead. We also delete the default .padding modifier. We initialize the Image by using the name of the first image file.

var body: some View {
    Image("burger")
}

The Image view currently covers the whole screen. To fix this, we have to apply a frame to it which serves as a fixed-sized container for our Image. We do this by applying the .frame modifier to our Image view and specifying the height and width of the frame. The .frame modifier is used to limit the dimensions (the “size”) of the corresponding view. To ensure that our Image does not exceed these limits we also add a .clipped modifier. The .clipped modifier cuts out the areas of the view that exceeds the specified frame.

Image("burger")
    .frame(width: 330, height: 135)
    .clipped()

Next, we need to resize the Image to make sure that it fits into the frame without distorting the dimensions of the original image file. For being able to scale an Image, we must apply the .resizable modifier first. It is important that the .resizable modifier is the first modifier of the Image view (The reason for this will be explained in a moment). 

Image("burger")
    .resizable()
    .frame(width: 330, height: 135)
    .clipped()

Next, we apply the .aspectRatio modifier to our Image (as the second modifier!). This modifier decides in which way the Image is scaled. For keeping the original dimensions of the image, we can choose between the .fill or .fit contentMode. “Fill” scales the image so that the entire frame gets filled out with the Image. “Fit” is used for making sure that the whole Image is visible inside the frame when being scaled. In our example, we use .fill as the contentMode.

Image("burger")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 330, height: 135)
    .clipped()

Next, we want to round off the corners of our Image view. We do this by applying the .cornerRadius modifier (as the last modifier!) to it.

Image("burger")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 300, height: 150)
    .clipped()
    .cornerRadius(10.0)

Finally, we add a slight drop-shadow to our Image to make it stand out more. For this, we use the .shadow modifier.

Image("burger")
    .resizable()
    .aspectRatio(contentMode: .fill)
    .frame(width: 330, height: 135)
    .clipped()
    .cornerRadius(10)
    .shadow(radius: 5)

This is how your ContentView should look like now:

For each category cell, we want to place a Text view with the name of the respective category on the corresponding Image view. To stack views in SwiftUI on top of each other, we use so-called ZStacks. You can embed your Image into such a ZStack by CMD-clicking on it and selecting “Embed in ZStack”.

ZStack {
    Image("burger")
        .resizable()
        .aspectRatio(contentMode: .fill)
        .frame(width: 330, height: 135)
        .clipped()
        .cornerRadius(10)
        .shadow(radius: 5)
}

All views within a ZStack are getting stacked on top of each other. Let’s stack a Text view on top of our Image view like this:

ZStack {
    Image("burger")
        //...
    Text("Burger")
}

To give our Text its own font and size we use the .custom option within the .font modifier. Let’s also change the color of the text to white.

Text("Burger")
    .font(.custom("HelveticaNeue-Medium", size: 25))
    .foregroundColor(.white)

However, we don’t want our Text view to be placed in the center of the Image view but in the left lower corner. To this, we change the alignment mode of our ZStack to .bottomLeading.

ZStack(alignment: .bottomLeading) {
    //...
}

Finally, we add some padding to our Text view.

Text("Burger")
    .font(.custom("HelveticaNeue-Medium", size: 25))
    .foregroundColor(.white)
    .padding(10)

Your ContentView should look something like this right now:

Excursus: How modifiers work and why their order is matters 💡

As we have already learned, we use modifiers to adjust the appearance and behavior of SwiftUI views. But what exactly happens to the view when we apply a modifier to it?

Take a look at the example above. We have initialized a Text view with the String “Burger”. Then we adjusted the font using the .font modifier. What happened is that the .font modifier took our Text view and generated a new view out of it, this time with a different font.

So what you should remember is that every time we apply a modifier to a view, not the original view itself is changed, but we get a new, modified view. After we apply another modifier, in our case the .foregroundColor modifier, we get a new view again with the old view (the view created by the .font modifier) as its reference.

You may wonder why this concept is relevant.

Try applying one more modifier to the Text view:

Text("Burger")
    .font(.custom("HelveticaNeue-Medium", size: 50))
    .foregroundColor(.white)
    .padding(10)
    .background(Color.gray.opacity(0.8))

Have a look at the preview simulator and then change the order of the last two modifiers.

Text("Burger")
    .font(.custom("HelveticaNeue-Medium", size: 50))
    .foregroundColor(.white)
    .background(Color.gray.opacity(0.8))
    .padding(10)

Do you notice the difference?

The first variant gives us a Text view with some padding which is filled with grey color. With the second variant, we get a Text view with a gray background which is extended by some padding.

This is because the .background modifier in the first variant utilizes the last created view, which is a Text view that already has some padding, and fills out the resulting view with a gray color. The .padding modifier of the second Text view also utilizes the last created view (but this time this view is a Text view with a grey background) and creates a new view out of it by applying some padding. 

Great, now that you understand how modifiers work and why their order affects the appearance of the associated view, you can remove .background modifier again.

Creating the List 💡

To avoid writing the same code four times we make it reusable by CMD-clicking on the ZStack, selecting “Extract Subview” and naming the extracted view “CategoryView”. If this option isn’t available, you can manually create a subview like this:

The ZStack is now outsourced as a separate view which we can easily insert into our body, as just shown.

Hint: Extracting views as subviews is best practice in SwiftUI and helps you make your code more reusable and clear.

We want to have four categories for our Food Delivery App. To achieve this, we use something called Lists in SwiftUI. A List is like a Table View (which maybe know if you worked with UIKit before) and contains multiple rows of data in a single column.

“List: A container that presents rows of data arranged in a single column.”

Let’s create such a List and embed our CategoryView inside it. To have a total of four category “cells” (it’s not quite accurate to call list rows “cells”), we have to embed four CategoryView instances into the List.

var body: some View {
    List {
        CategoryView()
        CategoryView()
        CategoryView()
        CategoryView()
    }
}

We want to get rid of the default “grouped” style of our List. To do this, we use the .plain style instead.

List {
    CategoryView()
    CategoryView()
    CategoryView()
    CategoryView()
}
    .listStyle(.plain)

We also want to get rid of the separator lines between the cells. To do this, we apply each CategoryView instance with the .listRowSeparator(.hidden) modifier.

List {
    CategoryView()
        .listRowSeparator(.hidden)
    CategoryView()
        .listRowSeparator(.hidden)
    CategoryView()
        .listRowSeparator(.hidden)
    CategoryView()
        .listRowSeparator(.hidden)
}
    .listStyle(.plain)

Note: The .listRowSeparator modifier is only available with iOS 15.0 or higher. Therefore, make sure your deployment info is set accordingly. You can check this by opening the settings of your project by clicking on the project file in the Project Navigator and opening the “General” tab.

You maybe noticed that the cells are not centered anymore. To change this, wrap the ZStack inside the CategoryView into an HStack. Next, insert one Spacer view above and one Spacer view below the ZStack.

struct CategoryView: View {
    var body: some View {
        HStack {
            Spacer()
            ZStack {
                //...
            }
            Spacer()
        }
    }
}

The cells are very close together. To increase the distances we add paddings above and below the cells. Thus, we add two custom .padding modifiers to the HStack of the extracted CategoryView like this:

struct CategoryView: View {
    var body: some View {
        HStack {
           //...
        }
            .padding(.top, 5)
            .padding(.bottom, 5)
    }
}

Your ContentView should look like this right now:

Making the list dynamic 🔄

Of course, not every CategoryView should contain the same Image with the same Text. To change this, we declare two properties in our CategoryView above its body – one String-type constant named “imageName” and another String-type constant named “categoryName”. We want to use these properties to initialize the Image and to insert a corresponding Text. The values for the properties will then be entered at the initialization of the CategoryViews

Thus, we edit our CategoryView accordingly and use the properties for our Image and the Text view instead of static values.

struct CategoryView: View {
    
    let imageName: String
    let categoryName: String
    
    var body: some View {
        HStack {
            Spacer()
            ZStack(alignment: .bottomLeading) {
                Image(imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 330, height: 135)
                    .clipped()
                    .cornerRadius(10)
                    .shadow(radius: 5)
                Text(categoryName)
                    .font(.custom("HelveticaNeue-Medium", size: 25))
                    .foregroundColor(.white)
                    .padding(10)
            }
            Spacer()
        }
            .padding(.top, 5)
            .padding(.bottom, 5)
    }
}

Next, we need to initialize the CategoryView instances inside the ContentView like this:

struct ContentView: View {
    var body: some View {
        List {
            CategoryView(imageName: "burger", categoryName: "Burger")
                .listRowSeparator(.hidden)
            CategoryView(imageName: "pizza", categoryName: "Pizza")
                .listRowSeparator(.hidden)
            CategoryView(imageName: "pasta", categoryName: "Pasta")
                .listRowSeparator(.hidden)
            CategoryView(imageName: "desserts", categoryName: "Desserts")
                .listRowSeparator(.hidden)
        }
            .listStyle(.plain)
    }
}

Great! This is how our Food Delivery App looks like so far:

Recap 🎊

Okay, let’s take a little break to recap what we’ve accomplished so far. We learned how to use and customize Image views in SwiftUI. We also learned how to use ZStacks. We used this knowledge to build a “blueprint“ cell. After doing this, we created a static List using that blueprint four times. After we’ve set everything up, we were ready to make our List dynamic by using properties instead of static values.

Next, we will set up and prepare the data model to feed our Food Delivery App with information about what meals can be ordered.

If you are ready, let’s go on!

5 replies on “Creating Lists in SwiftUI”

How can I select the ZStack in the preview canvas to “Extract Subview” (that’s how it’s called in my version 12.3 of Xcode)?
I only manage to select the image or the text but not the whole ZStack.

This seems to be a common problem (apple bug). When you run into this, you can create a new view like the author says or to save some time:
1) first embed the view(s) you want to extract into an HStack
2) now extract the original view you want to extract (should work now)
3) delete the empty HStack view you created in step (1)

How is the size of the ZStack determined? Does it grow to fit the largest of its children, or does it match the size of the image because its size is explicitly set?

Leave a Reply

Your email address will not be published. Required fields are marked *