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
andForEach
to build dynamic list-based UIs - Important aspects of
List
, likeIdentifiable
and its data source - Using
List
in conjunction withForEach
to build better lists - Removing objects from a list with
ForEach
andonDelete()
- Creating a sectioned list with nested
ForEach
andSection
Ready? Let’s go.
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.
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:
- An unnamed parameter
data
, which is the data we want to display in the List view. In our example, this data is themovies
array. - A keypath for the
id
parameter, which indicates theMovie
property we want to use to uniquely identify objects in themovies
array. (Theid
parameter is redundant here, you can use either/bothid
andIdentifiable
.) - 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)
}
}
}
}
}
How does this work?
- From a birds-eye view, you can see we’ve created a
List
with two nestedForEach
structures. We’re first iterating the sections, and for each section, we’re looping over the movies in it. - The
ForEach
structure forPeriod.allCases
will loop over all cases ofPeriod
. 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 aText
view that contains the period as a header. Keep in mind that these headers are created for each case of thePeriod
enum. -
Inside each
Section
, we’re creating anotherForEach
structure. This usesmovies.filter { ··· }
as its data source, which will return the items frommovies
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 withForEach
- 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: