SwiftUI Lists and Navigation

The SwiftUI List view provides a way to present information to the user in the form of a vertical list of rows. Often the items within a list will navigate to another area of the app when tapped by the user. Behavior of this type is implemented in SwiftUI using the NavigationView and NavigationLink components.

The List view can present both static and dynamic data and may also be extended to allow for the addition, removal and reordering of row entries.

This chapter will provide an overview of the List View used in conjunction with NavigationView and NavigationLink in preparation for the tutorial in the next chapter entitled A SwiftUI List and Navigation Tutorial.

1.1 SwiftUI Lists

The SwiftUI List control provides similar functionality to the UIKit TableView class in that it presents information in a vertical list of rows with each row containing one or more views contained within a cell. Consider, for example, the following List implementation:

struct ContentView: View {

    var body: some View {
        List {
            Text("Wash the car")
            Text("Vacuum house")
            Text("Pick up kids from school bus @ 3pm")
            Text("Auction the kids on eBay")
            Text("Order Pizza for dinner")
        }
    }
}

When displayed in the preview, the above list will appear as shown in Figure 25‑1:

Figure 25‑1

A list cell is not restricted to containing a single component. In fact, any combination of components can be displayed in a list cell. Each row of the list in the following example consists of an image and text component within an HStack:

List {
    HStack {
        Image(systemName: "trash.circle.fill")
        Text("Take out the trash")
    }
    HStack {
        Image(systemName: "person.2.fill")
        Text("Pick up the kids") }
    HStack {
        Image(systemName: "car.fill")
        Text("Wash the car")
    }
}

The preview canvas for the above view structure will appear as shown in Figure 25‑2 below:

Figure 25‑2

The above examples demonstrate the use of a List to display static information. To display a dynamic list of items a few additional steps are required.

1.2 SwiftUI Dynamic Lists

A list is considered to be dynamic when it contains a set of items that can change over time. In other words, items can be added, edited and deleted and the list updates dynamically to reflect those changes.

To support a list of this type, the data to be displayed must be contained within a class or structure that conforms to the Identifiable protocol. The Identifiable protocol requires that the instance contain a property named id which can be used to uniquely identify each item in the list. The id property can be any Swift or custom type that conforms to the Hashable protocol which includes the String, Int and UUID types in addition to several hundred other standard Swift types. If you opt to use UUID as the type for the property, the UUID() method can be used to automatically generate a unique ID for each list item.

The following code implements a simple structure for the To Do list example that conforms to the Identifiable protocol. In this case, the id is generated automatically via a call to UUID():

struct ToDoItem : Identifiable {
    var id = UUID()
    var task: String
    var imageName: String
}

For the purposes of an example, an array of ToDoItem objects can be used to simulate the supply of data to the list which can now be implemented as follows:

struct ContentView: View {

    var listData: [ToDoItem] = [
         ToDoItem(task: "Take out trash", imageName: "trash.circle.fill"),
         ToDoItem(task: "Pick up the kids", imageName: "person.2.fill"),
         ToDoItem(task: "Wash the car", imageName: "car.fill")
       ]

    var body: some View {
        List(listData) { item in
            HStack {
                Image(systemName: item.imageName)
                Text(item.task)
            }
        }
    }
}
.
.

Now the list no longer needs a view for each cell. Instead, the list iterates through the data array and reuses the same HStack declaration, simply plugging in the appropriate data for each array element.

In situations where dynamic and static content need to be displayed together within a list, the ForEach statement can be used within the body of the list to iterate through the dynamic data while also declaring static entries. The following example includes a static toggle button together with a ForEach loop for the dynamic content:

struct ContentView: View {

    @State var toggleStatus = true
.
.    
    var body: some View {
        List {
            Toggle(isOn: $toggleStatus) {
                Text("Allow Notifications")
            }

            ForEach (listData) { item in
                HStack {
                    Image(systemName: item.imageName)
                    Text(item.task)
                }
            }
        }
    }
}

Note the appearance of the toggle button and the dynamic list items in Figure 25‑3:

Figure 25‑3

A SwiftUI List implementation may also be divided into sections using the Section view, including headers and footers if required. Figure 25‑4 shows the list divided into two sections, each with a header:

Figure 25‑4

The changes to the view declaration to implement these sections are as follows:

List {
    Section(header: Text("Settings")) {
        Toggle(isOn: $toggleStatus) {
            Text("Allow Notifications")
        }
    }

    Section(header: Text("To Do Tasks")) {
        ForEach (listData) { item in
            HStack {
                Image(systemName: item.imageName)
                Text(item.task)
            }
        }
    }
}

Often the items within a list will navigate to another area of the app when tapped by the user. Behavior of this type is implemented in SwiftUI using the NavigationView and NavigationLink views.

1.3 SwiftUI NavigationView and NavigationLink

To make items in a list navigable, the first step is to embed the list within a NavigationView. Once the list is embedded, the individual rows must be wrapped in a NavigationLink control which is, in turn, configured with the destination view to which the user is to be taken when the row is tapped.

The NavigationView title bar may also be customized using modifiers on the List component to set the title and to add buttons to perform additional tasks. In the following code fragment the title is set to “To Do List” and a button labelled “Add” is added as a bar item and configured to call a hypothetical method named addTask():

NavigationView {
    List {
.
.
    }
    .navigationBarTitle(Text("To Do List"))
    .navigationBarItems(trailing: Button(action: addTask) {
        Text("Add")
     })
.
.
}

Remaining with the To Do list example, the following changes are necessary to implement navigation and add a navigation bar title:

var body: some View {

    NavigationView {
        List {
            Section(header: Text("Settings")) {
                Toggle(isOn: $toggleStatus) {
                    Text("Allow Notifications")
                }
            }

            Section(header: Text("To Do Tasks")) {
                ForEach (listData) { item in
                    HStack {
                        NavigationLink(destination: Text(item.task)) {
                            Image(systemName: item.imageName)
                            Text(item.task)
                        }
                    }
                }
            }
        }
        .navigationBarTitle(Text("To Do List"))
    }
}

In this example, the navigation link will simply display a new screen containing a Text view displaying the task string value. When tested in the canvas using live preview, the finished list will appear as shown in Figure 25‑5 with the title and chevrons on the far right of each row now visible. Tapping the links will navigate to the text view.

Figure 25‑5

1.4 Making the List Editable

It is common for an app to allow the user to delete items from a list and, in some cases, even move an item from one position to another. Deletion can be enabled by adding an onDelete() modifier to each list cell, specifying a method to be called which will delete the item from the data source. When this method is called it will be passed an IndexSet object containing the offsets of the rows being deleted. Once implemented, the user will be able to swipe left on rows in the list to reveal the Delete button as shown in Figure 25‑6:

Figure 25‑6

The changes to the example List to implement this behavior might read as follows:

.
.
List {
        Section(header: Text("Settings")) {
            Toggle(isOn: $toggleStatus) {
                Text("Allow Notifications")
            }
        }

        Section(header: Text("To Do Tasks")) {
            ForEach (listData) { item in
                HStack {
                    NavigationLink(destination: Text(item.task)) {
                        Image(systemName: item.imageName)
                        Text(item.task)
                    }
                }
            }
            .onDelete(perform: deleteItem)
        }
    }
    .navigationBarTitle(Text("To Do List"))
}
.
.
func deleteItem(at offsets: IndexSet) {
    // Delete items from data source here
}

To allow the user to move items up and down in the list the onMove() modifier must be applied to the cell, once again specifying a method to be called to modify the ordering of the source data. In this case, the method will be passed an IndexSet object containing the positions of the rows being moved and an integer indicating the destination position.

In addition to adding the onMove() modifier, an EditButton instance needs to be added to the List. When tapped, this button automatically switches the list into editable mode and allows items to be moved and deleted by the user. This edit button is added as a navigation bar item which can be added to a list by applying the navigationBarItems() modifier. The List declaration can be modified as follows to add this functionality:

List {
        Section(header: Text("Settings")) {
            Toggle(isOn: $toggleStatus) {
                Text("Allow Notifications")
            }
        }

        Section(header: Text("To Do Tasks")) {
            ForEach (listData) { item in
                HStack {
                    NavigationLink(destination: Text(item.task)) {
                        Image(systemName: item.imageName)
                        Text(item.task)
                    }
                }
            }
            .onDelete(perform: deleteItem)
            .onMove(perform: moveItem)
        }
    }
    .navigationBarTitle(Text("To Do List"))
    .navigationBarItems(trailing: EditButton())
}
.
.
func moveItem(from source: IndexSet, to destination: Int) {
    // Reorder items is source data here
}

Viewed within the preview canvas, the list will appear as shown in Figure 25‑7 when the Edit button is tapped by the user. Clicking and dragging the three lines on the right side of each row allows the row to be moved to a different list position (in the figure below the “Pick up the kids” entry is in the process of being moved):

Figure 25‑7

1.5 Summary

The SwiftUI List view provides a way to order items in a single column of rows, each containing a cell. Each cell, in turn, can contain multiple views when those views are encapsulated in a container view such as a stack layout. The List view provides support for displaying both static and dynamic items or a combination of both.

List views are usually used as a way to allow the user to navigate to other screens. This navigation is implemented by wrapping the List declaration in a NavigationView and each row in a NavigationLink.

Lists can be divided into titled sections and assigned a navigation bar containing a title and buttons. Lists may also be configured to allow rows to be added, deleted and moved.