SwiftUI – Mastering Table Views (Lists) #2

Share on facebook
Share on twitter
Share on pinterest

This is the second part of the Mastering Table Views (Lists) series, here is the first part!

Welcome to the second part of our Mastering Table Views (Lists) in SwiftUI series! In this series we will rebuild a Food Delivery App with Apple’s newest UI-Framework SwiftUI! We will get to know basic concepts of SwiftUI and learn how to apply them. If you don’t know what SwiftUI is and what you need in order to run it, have a look at this article.

Here is a video of the finished app we are going to build in this series.

In this part we will create a detailed view that contain the products of the category that is selected by the user. We will learn how to connect the detailed view with the main view and how to pass data through them.

Building our data model 🛠

Because we want to provide our detailed view with the several products, we have to create an according data model for handling that products.

Let’s create a new Swift file by clicking File – New – File and then selecting Swift File. Let’s call this file Food. Make sure the Foundation and SwiftUI Kit is imported. For handling our food products we create a class called Food inside this file.

import Foundation
import SwiftUI

class Food {

}

Each product should contain the product’s title and the price of the product. Therefore we create appropriate attributes.

class Food {

   let title: String
   let price: Double

}

Every Food instance should belong to one of the four categories: burger, pasta, pizza and dessert. To be able to select and distinguish between these four different options we have to create an enum.

“An enumeration is a group of values that are related […] The easiest way to think about enums is as structured lists of related items. A few examples: Colors: red, green, blue, purple, yellow, etc. Ice cream flavors: vanilla, chocolate, stracciatella, butter pecan, etc.”

learnappmaking.com

Let’s create a new file named Helper to place that enum in. We call the enum Categories and insert four different cases for our four categories.

enum Categories {
    case burger
    case pasta
    case pizza
    case dessert
}

Now we can switch back the our Food.swift file and insert an attribute category of the type Categories. Each instance created out of this class will now be assigned to a certain category.

class Food {
    
    let title: String
    let price: Double
    let category: Categories
    
}

Because we will use our Food data model to wrap multiple instances of it into a List later on, we need it to conform to the Identifiable protocol. This is required to pass custom class instances into Lists in SwiftUI (it’s also the reason why we imported the SwiftUI kit into our Food.swift file). The Identifiable protocol has only one mandatory requirement: It needs the class to contain an attribute (for the list) to identify every instance by a unique id. Therefore we simply declare an id attribute of the type Int.

class Food: Identifiable {
    
    let title: String
    let price: Double
    let category: Categories
    let id: Int
    
}

Our Food model now contains everything we need, so let’s add the according init function:

class Food: Identifiable {
    
    let title: String
    let price: Double
    let category: Categories
    let id: Int
    
    init(title: String, price: Double, category: Categories, id: Int) {
        self.title = title
        self.price = price
        self.category = category
        self.id = id
    }
    
}

Providing the product data ➡️

Now that we created our Food data model we are able to import our products which we will use to feed our detailed list.

We will use a separate file for this data, so create a new Swift file called FoodData.

Let’s use an array containing all the products we want for our app. Each product is a instance of the Food data model and must therefore be initialised with the according product title, price, category and id.

Feel free to copy and paste the array below into your FoodData.swift file. Of course you can edit this array to your wishes!


let foodData: [Food] = [
    Food(title: "Margherita", price: 5.99, category: .pizza, id: 1),
    Food(title: "Prosciutto", price: 6.89, category: .pizza, id: 2),
    Food(title: "Funghi", price: 6.99, category: .pizza, id: 3),
    Food(title: "Calzone", price: 6.99, category: .pizza, id: 4),
    Food(title: "BBQ Burger", price: 9.90, category: .burger, id: 5),
    Food(title: "Cheeseburger", price: 7.90, category: .burger, id: 6),
    Food(title: "Vegan Burger", price: 8.90, category: .burger, id: 7),
    Food(title: "Pulled Pork Burger", price: 11.90, category: .burger, id: 8),
    Food(title: "Spagetthi Bolognese", price: 8.90, category: .pasta, id: 9),
    Food(title: "Penne all'arrabbiata", price: 7.90, category: .pasta, id: 10),
    Food(title: "Aglio e olio", price: 7.90, category: .pasta, id: 11),
    Food(title: "Cheesecake", price: 3.99, category: .dessert, id: 12),
    Food(title: "Cupcake", price: 2.99, category: .dessert, id: 13),
    Food(title: "Icecream", price: 2.99, category: .dessert, id: 14)
]

Creating our food detail rows 🖌

Let’s create our UI for the detailed products view. We want to have a list again for this. As you learned in the last part of this tutorial, lists contain of rows with the data to display.

We first want to create the UI of these rows before we make a list out of them. Therefore we create a new file but now of the type SwiftUI View. Let’s call it DetailRow.swift.

Each DetailRow should contain three objects: A Text object containing the title of the product, another Text object containing the according price and a Button which notifies us when the user orders the product.

Let’s start with our first Text object. We can use the default Text object which currently contains “Hello World” for this, but change it to, for example, “BBQ Burger”. Don’t worry, we will make this dynamic by using our data model in a moment. But for now, let’s use static values for building up the UI.

Next, we want another Text object for the price of the product. The title Text and the price Text should be stacked vertically, so CMD-click on the Text object and select “Embed in VStack”. Now we can insert a second Text object containing the price of the food, for example, “10.00 $”. Both text object should be aligned on the “left side”, so we insert the .leading option for the alignment of the VStack.

struct DetailRow : View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("BBQ Burger")
            Text("10.00 $")
        }
    }
}

We want our title text object to be of a more emphasised look. Therefore we apply the .font modifier with the .headline option to it. We do the same with the price Text object but select the .caption option. Additionally, we want a little padding for the title Text, so we add a .padding modifier for the text to be 10 points away from the upper bound.

struct DetailRow : View {
    var body: some 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 object next to the both Text objects.


struct DetailRow : View {
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("BBQ Burger")
                    .font(.headline)
                    .padding(.top, 10)
                    Text("10.00 $")
                        .font(.caption)
                }
            Text("ORDER")
            }
    }
}

To make this new Text to be part of a Button, select it and click on “Embed in a Button”.

But let’s delete the onTrigger argument and instead insert the action parameter followed by curly braces manually. The action argument of a Button accepts a closure that contains what happens when the user taps on the Button. We want to be notified, therefore we write a appropriate print statement into the curly braces.

struct DetailRow: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("BBQ Burger")
                    .font(.headline)
                    .padding(.top, 10)
                    Text("10.00 $")
                        .font(.caption)
                }
            Button(action: {print("Order received")}) {
                    Text("ORDER")
                }
            }
    }
}

We want to change the Button’s size, so we apply a .frame modifier to it and insert the width and height we want for the Button. We also want an orange background for it that should be rounded. Therefore we apply the .background and .cornerRadius modifier to it.

struct DetailRow: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("BBQ Burger")
                    .font(.headline)
                    .padding(.top, 10)
                    Text("10.00 $")
                        .font(.caption)
                }
            Button(action: {print("Order received")}) {
                    Text("ORDER")
                }
                .frame(width: 80, height: 50)
                .background(Color.orange)
                .cornerRadius(10.0)
            }
    }
}

Last but not least, we want the Text inside the Button to be of a white font, so we insert a .foregroundColor modifier below the text object.

struct DetailRow: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("BBQ Burger")
                    .font(.headline)
                    .padding(.top, 10)
                    Text("10.00 $")
                        .font(.caption)
                }
            Button(action: {print("Order received")}) {
                    Text("ORDER")
                        .foregroundColor(.white)
                }
                .frame(width: 80, height: 50)
                .background(Color.orange)
                .cornerRadius(10.0)
            }
    }
}

To push both objects inside the HStack, the VStack containing the two Text objects and the Button, to the left and right edges of the screen we insert a Spacer between them. We also want all objects to be a little bit away from all edge, therefore we apply an overall .padding modifier

struct DetailRow: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text("BBQ Burger")
                    .font(.headline)
                    .padding(.top, 10)
                    Text("10.00 $")
                        .font(.caption)
                }
            Spacer()
            Button(action: {print("Order received")}) {
                    Text("ORDER")
                        .foregroundColor(.white)
                }
                .frame(width: 80, height: 50)
                .background(Color.orange)
                .cornerRadius(10.0)
            }
            .padding(20)
    }
}

Nice, we created the UI for our DetailRows. Now, let’s make the content of these rows dynamic!

Making the food details rows dynamic 🔄

Instead of static data, each row of our detailed food list should display the data of the certain Food instance. There, in our DetailRow class we have to declare a variable of the type Food above our body.

struct DetailRow: View {
    
    var food: Food
    
    var body: some View {
        //...
    }
}

This variable will be assigned to a Food instance for every row that will be initialised. In our RowView’s body we use the properties of the food variable for displaying the products title and price.

struct DetailRow : View {
    
    var food: Food
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(food.title)
                    .font(.headline)
                    .padding(.top, 10)
                    Text("\(food.price) $")
                        .font(.caption)
                }
            Spacer()
            Button(action: {print("Order received")}) {
                    Text("ORDER")
                        .foregroundColor(.white)
                }
                .frame(width: 80, height: 50)
                .background(Color.orange)
                .cornerRadius(10.0)
            }
            .padding()
    }
}

Note: Due to a bug in Xcode 11 beta, it’s currently not possible to round Double values to certain decimal places. I will update this as soon as there exist a solution!

Because we used a dynamic variable which is not yet assigned to a Food instance, we need to update our DetailRow_Previews struct and initialise a sample Food instance to provide the preview simulator with sample data to display. For this purpose we can simply choose the first object of our FoodData array.

Our UI should now look as follows:

Building our food detail list 🆕

Now that we created the UI for the rows displaying the products data, we can use these to build our view with the detailed list of the foods.

Let’s create a new SwiftUI file called DetailView and delete the default Text object. 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 the data it should contain as the input. This data must be distinguishable by an id. We did this by conforming the Food class to the identifiable protocol and assigning all instances inside the foodData array to unique id’s. Therefore we can simply insert the foodData array into our list.

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

Inside the curly braces of the List object we have to insert a closure which determines what happens with all the objects in the foodData array. We want to initialise one row for every object in this array. Therefore we write:

struct DetailView : View {
    
    var body: some View {
        List(foodData) { food in
            DetailRow(food: food)
        }
    }
    
}

Great! Our preview now contains a list with our DetailRows 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 list should only contain the objects of the foodData array which is of the currently selected category.

We therefore declare a variable of the Categories enum type which represents the category that the user selected on the main screen.

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

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

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

}

To filter the data we declare an empty array inside the functions body. What we now do is to cycle trough all objects of the foodData array and check for every element if it is of the same category as the input category. If this is true it adds the object to the filtered array. If our function cycled through element it eventually returns the filtered array to us.

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

Back to our DetailView: Instead of the whole foodData array we can now insert the filterData function into the lists which takes the currentCategory and then passes the filteredArray to the list.

We now have to update our DetailView_Previews struct again by determining a sample category.

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

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

Our preview should now look as follows:

Next, we have to connect the ContentView and the DetailView and pass the category selected by the user.

Connecting the views ⛓

To enable a connection from the ContentView to the DetailView we have to wrap the List of our ContentView containing the different CategoryViews into a so-called NavigationView.

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                CategoryView(imageName: "burger", categoryName: "BURGER")
                CategoryView(imageName: "pizza", categoryName: "PIZZA")
                CategoryView(imageName: "pasta", categoryName: "PASTA")
                CategoryView(imageName: "cake", categoryName: "DESSERTS")
            }
        }
    }
}

If we stack views into a NavigationView, like we stacked the List with its CategoryViews into a NavigationView, they become part of a navigation hierarchy which enables routing to other views like our DetailView.

“NavigationView: A view for presenting a stack of views representing a visible path in a navigation hierarchy.”

Apple

To enable routing to the DetailView by clicking on a CategoryView we have to wrap these into NavigationButtons. Let’s start by wrapping the “pizza” CategoryView into a NavigationButton. The destination of this NavigationButton is the DetailView. Because we are currently handling the “burger” CategoryView we want to initialise the DetailView with the .burger case of our Categories enum.

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: DetailView(currentCategory: .burger)) {
                    CategoryView(imageName: "burger", categoryName: "BURGER")
                }
                CategoryView(imageName: "pizza", categoryName: "PIZZA")
                CategoryView(imageName: "pasta", categoryName: "PASTA")
                CategoryView(imageName: "cake", categoryName: "DESSERTS")
            }
        }
    }
}

Let’s repeat this process for the three remaining CategoryViews.


struct ContentView : View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: DetailView(currentCategory: .burger)) {
                    CategoryView(imageName: "burger", categoryName: "BURGER")
                }
                NavigationLink(destination: DetailView(currentCategory: .pizza)) {
                    CategoryView(imageName: "pizza", categoryName: "PIZZA")
                }
                NavigationLink(destination: DetailView(currentCategory: .pasta)) {
                    CategoryView(imageName: "pasta", categoryName: "PASTA")
                }
                NavigationLink(destination: DetailView(currentCategory: .dessert)) {
                    CategoryView(imageName: "cake", categoryName: "DESSERTS")
                }
            }
        }
    }
}

We can now try out the functionality of our app by running the preview simulator in live mode. To do this, click on the play button next to the simulator. Great! We can now click on the different categories and a detailed list with all products of the selected category gets presented to us!

Handling the Navigation Bars 👁

Currently the Navigation Bar in our ContentView is empty. To change this add a .navigationBarTitle modifier to the List (not to the Navigation View!). We can now insert a Text object containing “Food Delivery”


struct ContentView : View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: DetailView(currentCategory: .burger)) {
                    CategoryView(imageName: "burger", categoryName: "BURGER")
                }
                NavigationLink(destination: DetailView(currentCategory: .pizza)) {
                    CategoryView(imageName: "pizza", categoryName: "PIZZA")
                }
                NavigationLink(destination: DetailView(currentCategory: .pasta)) {
                    CategoryView(imageName: "pasta", categoryName: "PASTA")
                }
                NavigationLink(destination: DetailView(currentCategory: .dessert)) {
                    CategoryView(imageName: "cake", categoryName: "DESSERTS")
                }
            }
                .navigationBarTitle(Text("Food Delivery"))
        }
    }
}

We also want a Navigation Bar title for our DetailView. But it should not display a static text, but instead the name of category the user selected. To do this we need to write a function that returns a appropriate string to us depending on the currentCategory. Let’s insert this function into our Helper.swift file


func categoryString (for category: Categories) -> String {
    switch category {
    case .pizza:
        return "Pizza"
    case .burger:
        return "Burger"
    case .pasta:
        return "Pasta"
    case .dessert:
        return "Desserts"
    }
}

We can now add a .navigationBarTitle modifier to the List of our DetailView and insert our created method as the Text object’s input. To make to Navigation Bar to be small we can use the .inline option as the .displayMode.


struct DetailView : View {
    
    var currentCategory: Categories
    
    var body: some View {
        List(filterData(by: currentCategory)) { food in
            DetailRow(food: food)
        }
            .navigationBarTitle(Text(categoryString(for: currentCategory)), displayMode: .inline)
    }
}

You can find the full source code on GitHub!

Conclusion 🎊

Perfect, let’s run our app again, but this time in the “normal” simulator! Everything works fine: we can select each category and get a list of all of the products of that category.

We are now finished with creating our Food Delivery app in SwiftUI! We learned a lot stuff like creating lists with custom rows, using data models to feed our lists and how to transfer data between different views.

I hope you enjoyed this tutorial. Make sure you subscribe to our newsletter to not miss any updates, a lot of stuff about SwiftUI is coming. Also visit us on Facebook and Instagram. If you have any questions do not hesitate to contact us or leave a comment below!

Leave a Reply

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

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book