Build A SwiftUI List App [UITableView]

iOS Swift

SwiftUI List is a container that has rows in a single column and you can arrange custom views inside of it.

List is the SwiftUI version of UITableview and it’s the easiest and fastest way of building list-style views. 🚀

Table of Contents

What You’ll Make by the End of this Tutorial

swfitui list

Create A SwiftUI Project

The first step is to enable SwiftUI in the User Interface option when creating a new project.

Rename Root View

By default, Xcode creates and sets the ContentView.swift file as a root view inside SceneDelegate.swift.

⚠️The folder structure might look different depending on when you read this.

Rename the ContentView.swift to LandmarkList.swift as well as the class name inside to make it a more meaningful name.

Change the root view from ContentView.swift to LandmarkList.swift in the SceneDelegate.swift file.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: LandmarkList())
        self.window = window
        window.makeKeyAndVisible()
    }
}

To see the changes in the preview provider instantly, add LandmarkList() initializer inside the previews property.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}

Download Assets

Download the assets folder I have prepared for you or feel free to use your own. Unzip it and then drag and drop it to the Assets.xcassets folder and you’re all set!

Structure Model Object & Data

Go ahead and create a Landmark.swift file by going to FileNewFileSwift File under the Source section in the iOS template tab.

It has two parts inside it.

  1. Landmark Structure
  2. landmarksData

1. Landmark structure will store all the data that we need to build this app.

import SwiftUI
struct Landmark{
    var title:String
    var country:String
    var imageName:String {return title}
    var thumbnailName:String {return title + "Thumbnail"}
    var flagName:String {return country }
}
  • title: The name of a landmark
  • country: The origin of a landmark
  • imageName: The value of this property will be the same as the title property which matches the actual image name.
  • thumbnailName: This will have the value of title with “Thumbnail” at the end to match the thumbnail image name.
  • flagName: Will return the value of the country property as they share the same name.

And the landmarksData, which is an array of Landmark Objects.

var landmarksData =  [
    Landmark(title: "Isle Of Skye", country: "Switzerland"),
    Landmark(title: "Steinweg", country: "Germany"),
    Landmark(title: "Alpine", country: "Austria"),
    Landmark(title: "Neuschwanstein", country: "Germany"),
    Landmark(title: "Mont St Michel", country: "France")
]

Create A-List

The first step is to get the data in.

Switch back to the LandmarkList.swift file and assign the landmarksData to the landmarks property.

Next, create a list view inside the body property with the List initializer and pass landmarks to it as a parameter.

Then, create a closure with the parameter called landmark, which will have a landmark object on each iteration.

Then, show the title by initializing Text() with an argument landmark.title which will become a row view of the List.

struct LandmarkList: View {
    var landmarks = landmarksData
    var body: some View {
        List(landmarks) { landmark in
            Text(landmark.title)
        }
    }
}

When we run the above code, we get an error.

When using a structure inside a List, it must conform to identifiable protocol and the id property is required for it.

The identifiable protocol is telling the List to identify each row uniquely using the id property.

So, add Identifiable text to the Landmark structure type to conform to the protocol and define a property called id with a value of UUID()

struct Landmark:Identifiable{
    var id = UUID()
    var title:String
    var country:String
    var imageName:String {return title}
    var thumbnailName:String {return title + "Thumbnail"}
    var flagName:String {return country }
}

The UUID() is a universally unique value that can be used to identify types, interfaces, and other items.

To see the changes in the preview provider, pass the test data to the LandmarkList() initializer.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList(landmarks:landmarksData)
    }
}

And the list will look like this!

Now we have titles on each row in a list. Let’s create a custom row view to match the final layout next.

🚀 As you can see how easy it’s to build a list type layouts using SwiftUI compared to UITableView dealing with datasource and delegate methods.

Build A Custom Row View

There are two types of structures that we need to build this custom row view.

  1. Generic Structures are the invisible containers such as HStack, VStack, Spacer, Group, Section, etc and they are used for arranging view structures.
  2. View Structures are the ones that are visible on the screen such as Text, Image, etc.

0. HStack will arrange it’s children in a horizontal line and it’s a row view container in this example.

1. Image will hold the profile image on the left.

2. VStack will arrange its children vertically.

3. Text views (the title & country name) will be inside the VStack and arranged vertically.

4. Spacer is an invisible container that will push any view declared after it to the right if it’s in the Horizontal stack or to the bottom if it’s in the vertical stack.

5. Image will hold the country flag icon on the right.

List(landmarks) { landmark in
    HStack {
        Image(landmark.thumbnailName)
        VStack {
            Text(landmark.title)
            Text(landmark.country)
        }
        Spacer()
        Image(landmark.flagName)
    }
}

And the magic is below.

We can enhance any of the view styles by applying modifiers to them.

Configure Views Using Modifiers

One or more modifiers can be chained to the view structure to get a different version of the original value.

  • Create a rounded corner to the thumbnail image using cornerRadius()
  • Make the title view bold using a bold() modifier
  • Change the country text color using a foreground() modifier.
Image(landmark.thumbnailName).cornerRadius(8)

VStack(alignment: .leading) {
   Text(landmark.title).bold()
   Text(landmark.country).foregroundColor(.gray)
}

Passing alignment to the VStack() initializer with the value of .leading will make all its children move to the left edge.

Add Sections to a List Using Nested ForEach

Changing the data structure will be the first step to accommodate adding a section to the list.

In the Landmark.swift file, define another structure called Category that will have a few properties.

struct Category:Identifiable {
    var id = UUID()
    var title:String
    var landmarks:[Landmark]
}

Then, declare another variable called CategoriesData which will have an array of Categories objects.

var CategoriesData =  [
    Category(
        title: "Hill",
        landmarks: [
            Landmark(title: "Isle Of Skye", country: "Switzerland"),
            Landmark(title: "Steinweg", country: "Germany"),
            Landmark(title: "Alpine", country: "Austria")
        ]
    ),
    Category(
        title: "Castle",
        landmarks: [
            Landmark(title: "Neuschwanstein", country: "Germany"),
            Landmark(title: "Mont St Michel", country: "France")
        ]
    )
]

Now we have data that can be easily converted into a list with sections.

Using the combination of nested ForEach with Section structure, we can create a list view with sections head quickly.

Iterate over CategoriesData using ForEach inside List and set the category.title to the header parameter in the Section initializer.

Use another ForEach loop to iterate over the category.landmarks which is an array of Landmark objects.

import SwiftUI
struct LandmarkList: View {
    var landmarks = CategoriesData
    var body: some View {
        List() {
             ForEach(CategoriesData) { category in
                Section(header: Text(category.title)) {
                    ForEach(category.landmarks) { landmark in
                        LandmarkRow(landmark:landmark)
                    } // ForEach
                } // Section
            } // ForEach
        } // List
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList(landmarks:CategoriesData)
    }
}

Create a new view layout called LandmarkRow and add the Customer Row View code in there rather than adding it inside the nested ForEach loop.

struct LandmarkRow: View {
    var landmark:Landmark
    var body: some View {
        HStack {
            Image(landmark.thumbnailName).cornerRadius(8)
            VStack(alignment: .leading) {
              Text(landmark.title).bold()
              Text(landmark.country).foregroundColor(.gray)
             }
            Spacer()
            Image(landmark.flagName)
        }
    }
}

Make sure to update the value of the landmark’s property at the top before running the app in the simulator.

Also, update it inside the preview editor code if you want to see the changes immediately.

Add Navigation Bar to a List

NavigationView is a generic and invisible structure.

Wrapping List with the NavigationView will create a navigation bar to the view at the top.

To add a title to the navigation, use the navigationBarTitle modifier to the List.

struct LandmarkList: View {
    var landmarks = landmarksData
    var body: some View {
        NavigationView {
            List() {
                ...
            }.navigationBarTitle(Text("Landmarks")) // List
        } // NavigationView
    }
}