Working with List in SwiftUI

Written by Abhinav Girdhar on December 8 2020 in App Development, iOS, SwiftUI

Working with List in SwiftUI

A List view in SwiftUI shows rows in a vertical, single column. It’s a scrollable list of data that you can interact with. In this tutorial, we’ll discuss how you can create and customize a List view with SwiftUI.

Here’s what we’ll get into:

  • How to use List and ForEach to build dynamic list-based UIs
  • Important aspects of List, like Identifiable and its data source
  • Using List in conjunction with ForEach to build better lists
  • Removing objects from a list with ForEach and onDelete()
  • Creating a sectioned list with nested ForEach and Section

Ready? Let’s go.

  1. Build a List in SwiftUI
  2. Creating Lists with ForEach
  3. Sectioned Lists with ForEach
  4. Further Reading

Build a List in SwiftUI

Let’s take a look at the List view in SwiftUI. The most basic setup for a list is actually pretty simple.

List(movies, id: \.id) { movie in
    Text(movie.title)
}

Consider that we’ve got an array of Movie objects, each with an id and a title. With the above code, you can display a List view with Text items for each of the movies in the array.

SwiftUI List view example in Xcode

In the above code we’re working with an array of Movie objects. Here’s the code for the Movie struct:

struct Movie: Identifiable {
    var id = UUID()
    var title:String
}

The Movie struct conforms to the Identifiable protocol. Any type that adopts the Identifiable protocol must have an id property, which provides a “stable” identity for the object. In essence, this means that the id property must be unique for each object. We’re using a UUID for that, but you could also use an integer index key from a database for example.

This is the array of Movie objects that the List view is based upon:

@State var movies = [
    Movie(title: "Episode IV – A New Hope"),
    Movie(title: "Episode V – The Empire Strikes Back"),
    Movie(title: "Episode VI – Return of the Jedi"),
    ···
]

Let’s take a closer look at the code we earlier used for the List view:

List(movies, id: \.id) { movie in
    Text(movie.title)
}

The List view initializer has 3 parameters:

  1. An unnamed parameter data, which is the data we want to display in the List view. In our example, this data is the movies array.
  2. A keypath for the id parameter, which indicates the Movie property we want to use to uniquely identify objects in the movies array. (The id parameter is redundant here, you can use either/both id and Identifiable.)
  3. A closure for the rowContent parameter; the contents of individual list rows. Because it’s the last parameter, we can use trailing closure syntax.

That last parameter is the most interesting. It’s the closure that provides the content for each row in the List. Similar to map(_:), it’s called for every item in movies and it should return a view to display. In the above code, we’re simply returning a Text view. We’re also making use of the provided movie object.

If you want to follow along with the steps in this tutorial, you can create a new Xcode project. Choose the App template, SwiftUI for Interface, and SwiftUI App for Lifecycle. You can work in the default view ContentView, or make your own. Don’t forget to update it in the App struct!

Creating Lists with ForEach

The List view is essentially a container for subviews that can be scrolled. It’s similar to views like VStack and HStack, and UITableViewController for Storyboard-based apps.

You can, in fact, create a plain ol’ static List view from a few subviews:

List {
    Text("The Fellowship of the Ring")
    Text("The Two Towers")
    Text("The Return of the King")
}

What makes List special, is that it can create subviews dynamically using a data source and a bit of code that provides subviews for individual items. And in SwiftUI, you can combine List with the ForEach to do even more.

Here’s an example:

List {
    ForEach(movies, id: \.id) { movie in
        Text(movie.title)
    }
}

In the above code, we’ve separated the previous parameters for List and added them to the ForEach structure.

The ForEach structure computes views on demand from the movies collection, similar to how List did that. These views are subviews of List, which means you get the same result as you would with only using List.

So what’s the benefit of using ForEach? First off all, ForEach functions more like a generator or data provider than a view. Using ForEach also gives you access to data actions like onDelete(), onInsert() and onMove(), which List cannot.

The ForEach structure can be used with any kind of view that can contain multiple subviews, like stacks. Here’s an example:

HStack {
    ForEach(["File", "Edit", "View"], id: \.hash) { title in
        Text(title)
    }
}

You can also use ForEach with onDelete(), to remove items from the data source underlying the List. Here’s an example:

List {
    ForEach(movies, id: \.id) { movie in
        Text(movie.title)
    }
    .onDelete { indexSet in
        for index in indexSet {
            movies.remove(at: index)
        }
    }
}

The onDelete() modifier is called when you swipe a list item to the right, as you would do to delete an item on iOS. It’s an affordance that’s also built in the ubiquitous table view, and of course, List has it too.

For this to work, though, you’ll need to mark the data source as state. In our example, that means we’ll have to add the @State property wrapper to the movies property.

@State var movies = ···

The view is now dependent on the state of movies, which means that if you remove an item from the movies array, the view now updates too.

You don’t have to code the view for the list’s rows inside the List. You can also abstract them into a separate view, and reference the view in the list. Something like List(···) { MovieRow() }.

Sectioned Lists with ForEach

If List will just display a bunch of rows in a list, and ForEach will generate views based on a data source… that means we can use both to create sectioned lists. Let’s find out how!

First, we’re going to make a few changes to the Movie objects. Like this:

struct Movie: Identifiable {
    var id = UUID()
    var title:String
    var period:Period
}

The Movie struct now has a property period, which we’ll use to create a sectioned list. Each movie period gets its own list section.

Here’s the Period enumeration:

enum Period: String, CaseIterable {
    case original = "Original trilogy"
    case prequel = "Prequel trilogy"
    case sequel = "Sequel trilogy"
    case standalone = "Standalone"
}

The enum uses raw values, so we can use them as the Period type and as String values. The CaseIterable protocol lets us iterate the enum as an array, via its allCases property.

Next up, we’ll need some movies of course!

var movies = [
    Movie(title: "Episode IV – A New Hope", period: .original),
    Movie(title: "Episode V – The Empire Strikes Back", period: .original),
    Movie(title: "Episode VI – Return of the Jedi", period: .original),
    ···
    Movie(title: "Star Wars: The Clone Wars", period: .standalone),
    Movie(title: "Rogue One", period: .standalone),
    Movie(title: "Solo", period: .standalone),
    Movie(title: "The Mandalorian", period: .standalone)
]

Alright, now for that sectioned list. Here’s the code that we’re using:

List {
    ForEach(Period.allCases, id: \.rawValue) { period in
        Section(header: Text(period.rawValue)) {
            ForEach(movies.filter { $0.period == period }) { movie in
                VStack {
                    Text(movie.title)
                    Text(movie.period.rawValue)
                }
            }
        }
    }
}

Example of nested ForEach to make a sectioned List

How does this work?

  • From a birds-eye view, you can see we’ve created a List with two nested ForEach structures. We’re first iterating the sections, and for each section, we’re looping over the movies in it.
  • The ForEach structure for Period.allCases will loop over all cases of Period. They’re uniquely identified by their raw value, i.e. by their string. (For lack of a better alternative; this won’t work for duplicate strings!)
  • The Section view provides a grouping of one header and multiple rows. You can see we’re using a Text view that contains the period as a header. Keep in mind that these headers are created for each case of the Period enum.
  • Inside each Section, we’re creating another ForEach structure. This uses movies.filter { ··· } as its data source, which will return the items from movies for which their period is the same as the section’s period.
  • Finally, on the innermost level, we’re creating 2 simple Text views to display the movies name and the period they belong to. Neat!

It’s smart to remember at this point that SwiftUI is a declarative language to build UIs. We’re really just describing the hierarchy and structure of the User Interface that we want, and what data SwiftUI should base the UI upon. SwiftUI takes care of the rest.

Even though we’re using dynamic constructs like ForEach, the views are fixed and static until the data changes. We might as well have put all these movies, sections and stacks into one big hierarchy of code. The ForEach is there for us, the programmer, so we can code less and build more. Awesome!

Further Reading

In this tutorial, we’ve discussed how List works and how you can use it to build row/column-like layouts in SwiftUI. Here’s what you learned:

  • How you can use List to build UIs
  • Why a list’s data source’s objects must be Identifiable
  • Using List together with ForEach
  • How to remove objects from List and respond with @State
  • Building a sectioned list with nested ForEach

Want to learn more? Check out these resources: