Creating the DetailView

Creating our food detail rows 🖌

It’s time to create the interface for the detailed products view. Again, we want to use a List for this. The resulting “DetailView” containing this List will look like this:

First, we need to compose the interface of the rows before we embed them into a new List. For this purpose, create a new SwiftUI View file. Let’s name this file “DetailRow.swift”.

Each DetailRow should contain three views: a Text with the name of the product, a Text with the price of the product and a Button which the user can tap and which notifies us when an order has been placed.

Let’s start with our first Text view. We can use the default “Hello World” Text for this. However, we replace the corresponding String with – for example – “BBQ Burger” and remove the .padding modifier. Don’t worry, we will make this Text dynamic by using our data model in a moment. But for now, let’s use static values for building up the UI.

By the way, we can tell our preview simulator to always be only as large as needed to display the DetailRow view by adjusting the .previewLayout to .sizeThatFits:

Next, we want to add another Text view for the price of the product. The title Text and the price Text should be stacked vertically. To do this, CMD-click on the current Text view and select “Embed in VStack”.

VStack {
    Text("BBQ Burger")
}

Now we can add a second Text view with the price of the food, for example, “10.00 $”. Both Text views should be aligned on the “left side”. To do this, we use the .leading option for the alignment mode of the VStack.

VStack(alignment: .leading) {
    Text("BBQ Burger")
    Text("10.00 $")
}

We want to emphasize our title Text view. Therefore, we apply the .font modifier with the .headline option to it. We do the same with the price Text view but with the .caption option. Additionally, we want to add some padding to the title Text view.

VStack(alignment: .leading) {
    Text("BBQ Burger")
        .font(.headline)
        .padding(.top, 10)
    Text("10.00 $")
        .font(.caption)
}

Next, let’s create the “Order” Button. To do this, we have to embed the VStack into an HStack. CMD-click on the VStack and select “Embed in HStack”. Now we can insert another Text view next to our existing ones.

HStack {
    VStack(alignment: .leading) {
        //...
    }
    Text("ORDER")
}

To convert this Text into a touchable Button, wrap the Text view into a Button view like this:

Button(action: {
    print("Order received.")
}) {
    Text("ORDER")
}

The action parameter of a Button accepts a closure where we can specify what should happen when the user taps on the Button. Since we want to be notified when the user taps on the Button, we write a corresponding print statement into the curly braces.

We want to adjust the Button’s size. Thus, we apply a .frame modifier to it and specify the width and height. We also want an orange background with rounded corners for our background. Therefore, we apply the .background and .cornerRadius modifier to it.

Button(action: {
    print("Order received.")
}) {
    Text("ORDER")
}
    .frame(width: 80, height: 50)
    .background(Color.orange)
    .cornerRadius(10)

Last but not least, we want to change the font of the Text inside our Button to white. We do this by applying the .foregroundColor to this Text view.

Button(action: {
    print("Order received.")
}) {
    Text("ORDER")
        .foregroundColor(.white)
}
    .frame(width: 80, height: 50)
    .background(Color.orange)
    .cornerRadius(10)

To push both views inside the HStack (the VStack and the Button) to the left and right edges of the screen, we insert a Spacer between them. We also want to make sure that all views inside the HStack have enough distance to the edges of the screen. Therefore, we apply an overall .padding modifier to it.

HStack {
    VStack(alignment: .leading) {
        //...
    }
    Spacer()
    Button(action: {
        print("Order received.")
    }) {
        Text("ORDER")
            .foregroundColor(.white)
    }
    //...
}
    .padding(20)

Nice! We created the UI for our DetailRow.

Let’s replace the static values used by the Text views with dynamic properties.

Making the DetailRow dynamic 🔄

Instead of static values, each DetailRow should display the data of a certain Food instance. Thus, we have to declare a corresponding property of the type Food above the DetailRow‘s body.

struct DetailRow: View {
    
    let food: Food
    
    var body: some View {
            //...
        }
            .padding(20)
    }
}

Because we use a property that is not yet assigned to a Food instance, we need to update our DetailRow_Previews struct and initialize a sample Food instance to provide the Preview simulator with sample data to display. For this purpose, we can simply use the first object inside our FoodData array.

struct DetailRow_Previews: PreviewProvider {
    static var previews: some View {
        DetailRow(food: foodData[0])
            .previewLayout(.sizeThatFits)
    }
}

This food property will be assigned to a specific Food instance for every row that will be initialized inside the List we will create later on. In our DetailRow‘s body, we use the information the food property contains for displaying the product’s title and price.

struct DetailRow: View {
    
    let food: Food
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(food.title)
                    .font(.headline)
                    .padding(.top, 10)
                Text("\(food.price, specifier: "%2.2f") $")
                    .font(.caption)
            }
            Spacer()
            //...
        }
            .padding(20)
    }
}

Note: We use the specifier argument within the String interpolation to round off each food.price value to two decimal places.

Building up our DetailView List 🆕

Now that we have prepared our DetailRow view, we can use it to create a List of meals that can be ordered.

Let’s create a new SwiftUI file, name it “DetailView” and delete the default Text view and .padding modifier. For now, this view should contain a List with all the objects inside the FoodData array. Let’s create such a List:

struct DetailView: View {
    var body: some View {
        List() {
            
        }
    }
}

The List needs some data as its input. This data must be distinguishable by an id property. We did this earlier by conforming the Food struct to the Identifiable protocol and assigning all instances inside the foodData array to unique IDs. Therefore, we can simply insert the foodData array into our List.

List(foodData) {
            
}

Inside the curly braces of the List, we have to insert a closure where we specify how we want to utilize the objects in the foodData array. We want to initialize one row for every object in this array. Therefore we write:

List(foodData) { food in
    DetailRow(food: food)
}

Great! Now our DetailView contains a List with one DetailRow for every object in our FoodData array!

Of course, the DetailView should only display the foods of the category which the user selected. Thus, the related List should only contain the objects of the foodData array that are belonging to the currently selected category.

Therefore, we declare a property of the Categories enum type that represents the category that the user selected in the ContentView.

struct DetailView: View {
    
    let currentCategory: Categories
    
    var body: some View {
        //...
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView(currentCategory: .burger)
    }
}

To filter the data of the foodData array by the selected category we have to create a helper function. We add a function named filterData to our Helper.swift file. This function uses the selected category as its input and returns a new array containing only the filtered food data.

func filterData(foodDataSet: [Food], by category: Categories) -> [Food] {
    
}

To filter the data, we declare an empty array inside the function’s body. What we do now is to cycle through all the objects inside the foodData array. For every element, we check if it belongs to the same category as the input category. If this is true, we add the object to the filtered array. If our function cycled through all elements, we eventually return the filtered array.

func filterData(foodDataSet: [Food], by category: Categories) -> [Food] {
    var filteredArray = [Food]()
    
    for food in foodDataSet {
        if food.category == category {
            filteredArray.append(food)
        }
    }
    
    return filteredArray
}

Back to our DetailView: instead of using the whole foodData array we can now insert the filterData function into the Lists which utilizes the currentCategory and then passes the filteredArray to the List.

struct DetailView: View {
    
    let currentCategory: Categories
    
    var body: some View {
        List(filterData(foodDataSet: foodData, by: currentCategory)) { food in
            DetailRow(food: food)
        }
    }
}

Our Preview should now look like this:

Next, we connect the ContentView and the DetailView using a NavigationView.

7 replies on “Creating the DetailView”

Hi, I have a question, after I added the filterData function to the List in DetailView, I received the following error: “cannot find ‘filterData’ in scope”

Hi, my button has background color of Orange. but it seems like there is white background surrounding the Text. As a result, it looks like there is a white box around text, which is again inside a orange box

problem solved, It seems when I use Mac OS as preview target. this problem happens. but if I target iOS. everything is fine

I found the error!! inside the Food.swift the struct should add “Identifiable, Hashable”

like this :

import SwiftUI

struct Food: Identifiable, Hashable {

let id = UUID()
let title : String
let price : Double
let category : Categories

}

and it works!

Leave a Reply

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