In this blog post, I will dive deeply into one of the most important components of iOS development – the SwiftUI List View. We’ll look at how to create lists, include custom cells, style them with different list styles, and add selection of individual elements. This is vital information for any iOS app developer using SwiftUI. So let’s get started!
Download the project files from here ⬇️
What is a SwiftUI List and why is it important?
A list view is a container view that shows its content in a single scrollable column. This list can be constructed using hierarchical, dynamic, and static data. It provides a great-looking appearance that conforms to the standard styling for each of the different Apple platforms like iOS, macOS, and watchOS. Making it easy to build cross-platform apps.
Custom Styling is somewhat limited. Newer features include changing separator color and background colors for the list and cells in a different section.
It’s important to use the list view in SwiftUI as they provide a way to structure and manage data while allowing lazy loading of the list view itself instead of preloading the entire dataset every time. This is beneficial as it allows you to handle large datasets with only a fraction of the memory load compared to other approaches. It also helps to improve user experience by providing a more responsive and snappy UI.
Alternatively, you can look at ScrollView if you need horizontal scrolling. It can also be with LazyVGrid
and LazyHGrid
to create image gallery-style views. If you are making a macOS app, you might also consider Table because it offers a multi-column list with the same look as the macOS Finder app.
How to create a static List View in just a few lines
Working with a static list is very simple. You can define the fixed rows inside the List.
struct ContentView: View {
var body: some View {
List {
Text("First Row")
Text("Second Row")
Text("Third Row"
}
}
}
In the following example, we create a settings view. The default list style on iOS is InsetGroupedListStyle, which will give you the same appearance as Form. Styling is also adjusted for control views, like buttons, pickers, and menu items.
import SwiftUI
enum AppearanceStyle {
case dark, light, auto
}
struct SettingView: View {
@State var username: String = ""
@State var isPrivate: Bool = true
@State private var profileImageSize = false
@State private var fontSize: CGFloat = 5
@State private var appearance: AppearanceStyle = .auto
var body: some View {
List {
Section {
TextField("Username", text: $username)
Toggle("Private Account", isOn: $isPrivate)
Button {
} label: {
Text("Sign out")
}
} header: { Text("Profile") }
Section {
Slider(value: $fontSize, in: 1...10) {
Label("Default Font Size", systemImage: "text.magnifyingglass")
}
Picker("Appearance", selection: $appearance) {
Text("Dark").tag(AppearanceStyle.dark)
Text("Light").tag(AppearanceStyle.light)
Text("Auto").tag(AppearanceStyle.auto)
}
} header: { Text("Appearance") }
Section {
HStack {
Text("Version")
Spacer()
Text("2.2.1")
}
} header: { Text("ABOUT") }
}
}
}
However, on macOS List and Form look notably different. Form becomes a vertical stack without scrolling. Apple recommends following platform conventions and not creating section headers for mac. Use Form if you want the default appearance on mac for settings:
Dynamic List View
SwiftUI lists are most often created dynamically from an existing data source. The following example defines the data structure of a food item:
struct Food {
var name: String
var icon: String
var isFavorite: Bool
static func preview() -> [Food] {
return [Food(name: "Apple", icon: "🍎", isFavorite: true),
Food(name: "Banana", icon: "🍌", isFavorite: false),
Food(name: "Cherry", icon: "🍒", isFavorite: false),
Food(name: "Mango", icon: "🥭", isFavorite: true),
Food(name: "Kiwi", icon: "🥝", isFavorite: false),
Food(name: "Strawberry", icon: "🍓", isFavorite: false),
Food(name: "Graps", icon: "🍇", isFavorite: true)
]
}
}
For simplicity, I used a system icon instead of an image property of type SwiftUI image. The static function preview is a helper function to work with the Xcode preview, where we have to provide sample data.
Identifiable Protocol and Key Path
If you would now try to show a list of food items, you would face a build error ‘Initiazer init() requires that Food conforms to Identifiable’. You have 2 options to fix this error. First, you can specify a property called id of the type key path. The list uses the id property to identify the individual item rows by specifying a key path. That means what property of your data is unique and can be used to identify exactly which data item is used.
struct ContentView: View {
let foods = Food.preview()
var body: some View {
List(foods, id: \.name) { food in
Text(food.name)
}
}
}
You have to be careful when choosing the property. In our example, each food item should only appear once. Because you never know how you change your data model, it is safer to add unique identifier like UUID type. For the Food item, we could change to:
struct Food: Identifiable {
var name: String
var icon: String
var isFavorite: Bool
let id = UUID()
...
}
Notice I added conformance to the Identifiable Protocol which has required a property called id. This allows the list to directly know how to identify each data entry. We can simplify the list of food items further. You no longer have to specify the id key path. This is also used for ForEach:
struct ContentView: View {
let foods = Food.preview()
var body: some View {
List(foods) { food in
Text(food.name)
}
}
}
Complex Lists with Dynamic and Static Elements
You can combine static and dynamic elements in a List. For the dynamic data add a foreach inside the list. This is also useful if you have multiple data arrays that you want to show in the list. In the following code, I have 2 arrays for fruits and unhealthy food.
struct ContentView: View {
@State private var foods = Food.preview()
@State private var unhealthFoods = [Food(name: "Pizza", icon: "🍕", isFavorite: false),
Food(name: "Burger", icon: "🍔", isFavorite: false)]
var body: some View {
List {
ForEach(foods) { food in
Text(food.name)
}
ForEach(unhealthFoods) { food in
Text(food.name)
}
Button {
foods.append(Food(name: "new food", icon: "\(foods.count)", isFavorite: false))
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
Create a Custom Cell
If you have a background in UIKit, you probably want to create a custom cell. SwiftUI treats the view that is inside the List or ForEach as the custom cell. You don’t need to set up a cell as you would do for UITableView and UICollectionView. However if your views become too long and complex, it can be advantageous to create a separate subview for the custom cell.
In the previous example, I used the Food type which I need to pass to the custom view. An example where we show the icon, name, and favorite properties could be like this:
struct ContentView: View {
@State private var foods = Food.preview()
var body: some View {
List (foods) { food in
FoodRow(food: food)
}
}
}
struct FoodRow: View {
let food: Food
var body: some View {
HStack {
Text(food.icon)
Text(food.name)
Spacer()
Image(systemName: food.isFavorite ? "heart.fill" : "heart")
}
}
}
How to Further Structure the Content of Your List
When the underlying data becomes more complex, we need to look at options to organize our lists better. Continuing with the example, I had 2 arrays for healthy and unhealthy foods. Let’s look at how we can group them into different sections.
Sections with Headers and Footers
You can embed any view inside the List in a Section. In order to see these sections, choose ListStyles that are grouped. We use the default, which is InsetGroupedListStyle. Additionally, you can specify sections with a custom header and footer.
struct ContentView: View {
@State private var foods = Food.preview()
@State private var unhealthyFoods = [Food(name: "Pizza", icon: "🍕",
isFavorite: false),
Food(name: "Burger", icon: "🍔",
isFavorite: false)]
var body: some View {
List {
Section {
ForEach(foods) { food in
FoodRow(food: food)
}
}
Section {
ForEach(unhealthyFoods) { food in
FoodRow(food: food)
}
} header: {
Text("Unhealthy Foods")
} footer: {
Text("You should try to avoid them and only eat them occacionally.")
}
Button {
let newFood = Food(name: "New",
icon: "\(foods.count)",
isFavorite: false)
withAnimation {
foods.append(newFood)
}
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
Collapsable Sections with Disclosure Group
In cases where the underlying data is very complex, you might want to allow the user to collapse each section. When you change the list style to SidebarListStyle, the sections get a button next to the header that allows collapsing the sections.
In case you don’t want to sue SidebarListStyle or you only want to allow collapsing for certain sections disclosure group is a good option. Per default, the group is collapsed.
But you can use the input for isExpanded and pass a binding for a Boolean state property if you want to set the initial value to true.
struct ContentView: View {
@State private var foods = Food.preview()
@State private var unhealthyFoods = [Food(name: "Pizza", icon: "🍕",
isFavorite: false),
Food(name: "Burger", icon: "🍔",
isFavorite: false)]
var body: some View {
List {
Section {
DisclosureGroup("Healthy Foods") {
ForEach(foods) { food in
FoodRow(food: food)
}
}
}
Section {
DisclosureGroup("Unhealthy Foods") {
ForEach(unhealthyFoods) { food in
FoodRow(food: food)
}
}
}
Button {
foods.append(Food(name: "New",
icon: "\(foods.count)",
isFavorite: false))
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
Hierarchical list with tree structured data
If you need to show tree structured data with an arbitrary depth, you can use hierarchical lists. For example, we have an email app and want to show a folder structure with many subfolders. Each folder can have subfolders and email contents. The data is nested with the children parameter. The nesting property must be of the same type as the parent entity.
struct FileItem: Hashable, Identifiable {
var title: String
let isFolder: Bool
var children: [FileItem]? = nil
let id = UUID()
static func preview() -> [FileItem] {
[FileItem(title: "Inbox",
isFolder: true,
children: [FileItem(title: "My first email",
isFolder: false),
FileItem(title: "My second email",
isFolder: false)]),
FileItem(title: "Archived",
isFolder: true,
children: [FileItem(title: "work",
isFolder: true,
children: [FileItem(title: "Next Task", isFolder: false)]),
FileItem(title: "personal",
isFolder: true,
children: [FileItem(title: "Your Subscription expired", isFolder: false)])]),
FileItem(title: "Trash", isFolder: true)]
}
}
In order to show a hierarchical list, use the children property where you have to specify the key path for the nesting. In my case, I use the children property of the FileItem type.
Per default, all levels are collapsed. You will always see one top group in the list, even if you use a grouped list style.
import SwiftUI
struct ContentView: View {
@State private var fileItems = FileItem.preview()
var body: some View {
List(fileItems, children: \.children) { fileItem in
if fileItem.isFolder {
Label(fileItem.title, systemImage: "folder.fill")
} else {
Label(fileItem.title, systemImage: "envelope")
}
}
}
}
How to customize lists with different styling options in SwiftUI
Lists have a default style if not specified otherwise. I encountered some problems when using the live preview, so make sure you check by building the project. SwiftUI picks the style depending on the platform and situation. A list as the master view (left column) in a NavigationView will be shown in SidebarListStyle. The detail view will use InsetGroupedListStyle for iOS 15+ and for iOS 14 PlainListStyle. In the following code, I used the same SwiftUI view for both master and detail:
struct EnvironmentListView: View {
var body: some View {
NavigationView {
ContentView()
ContentView()
}
}
}
You can change the list appearance by specifying the list styling with the list styling view modifier:
List {
...
}
.listStyle(.inset)
Depending on the platform different styling options are also available. On iOS, you can use a grouped and inset grouped list style, whereas on macOS you can display a bordered list style and set an alternating row background. The following table gives an overview of the list of styling options for the different platforms:
Further Customization
You can change the list row insets with the listRowInsets view modifier. The default list row height is set in the environment. If you want to override the default values you can change the environment value like:
List {
...
}
.environment(\.defaultMinListRowHeight, 50)
.environment(\.defaultMinListHeaderHeight, 60)
New features are modifiers for hiding row and section separators with listSectionSeparator and listRowSeparator (iOS 15+ and macOS 13) in GroupedListStyle. You can also change the separator color with listRowSeparatorTint and listSectionSeparatorTint. You can increase the size of the section header by adding headerProminence(.increased). The background color of a row can be adjusted with listRowBackground (iOS 13+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+). If you want to change the color of the list background, you can first hide the content background and then add your own background:
List {
...
}
.scrollContentBackground(.hidden)
.background(Color.mint)
You can learn how I did the custom styling in this blog post: Customise List View Appearance in SwiftUI
Adding Interactivity to a List View
List offers an easy way to select a single row. You can use the init with the selection parameter, which takes a binding to a selection value. This works only if you make your data type conform to Hashable.
struct Food: Identifiable, Hashable {
let id = UUID()
...
}
Use a state property to declare the selection element, which in this example is of type optional UUID. The identifier of the Food type is UUID. Make sure the identifier and selection have the same type. Unfortunately, using the Food type itself for the selection does not work.
struct ContentView: View {
@State private var healthyFoods = Food.preview()
@State private var selectedFoodId: UUID? = nil
var body: some View {
List(healthyFoods, selection: $selectedFoodId) { food in
FoodRow(food: food)
}
}
}
List view also supports multiple selections. You have to change the selection variable to a collection type. On iOS, you can only select multiple elements in edit mode. Adding the edit button in the toolbar allows the user to switch to edit mode. Selection does not work with the foreach view.
struct ContentView: View {
@State private var foods = Food.preview()
@State private var selectedFoodIds: Set<UUID> = []
var body: some View {
NavigationView {
List(foods, selection: $selectedFoodIds) { food in
FoodRow(food: food)
}
.navigationTitle("Foods")
#if os(iOS)
.toolbar {
EditButton()
}
#endif
}
}
}
Further interactions for editing text inside the list cell, adding and removing data, refreshing list data with pull to refresh, and search interactions are also available.
Conclusion
SwiftUI provides a powerful and intuitive way to create beautiful and functional lists of data that your users can interact with. You can create hierarchical, static lists, and dynamic lists. It is also possible to make list selectable for single or multiple list items. It is important to make the list data`s identifiable to get the list automatically and correctly updated. Finally, SwiftUI’s styling tools give you the power to customize the look and feel of SwiftUI lists, so they represent your app’s unique style.
Thanks for reading! We hope this article has helped you understand SwiftUI lists better.
Download the project files from here ⬇️
Further Reading
- Read more about Loops in SwiftUI with ForEach
- Customise List View Appearance in SwiftUI
- See what layout you can create with ScrollView
- For more complex data with multiple columns check out Table
FAQ
How do I create a horizontal list in SwiftUI?
The List view does not have an option for horizontal scrolling. You can only create a horizontally scrollable view with ScrollView by changing the default input argument for scroll direction to horizontal.
How do I make a list scroll programmatically in SwiftUI?
You are out of luck if you want to programmatically scroll inside a List. This is only possible with ScrollView. With iOS 14 and macOS 11, ScrollViewReader was introduced, which gives you the ScrollViewProxy. Call scrollTo(_ id: ID, anchor: UnitPoint) to programmatically scroll to the view with the provided id.
Karin, excellent material, as always. Love your tutorials and examples.
Thanks,
Luis
I like what you guys are up too. This type of clever work and coverage!
Keep up the awesome works guys I’ve added you guys to our blogroll.
I’m not sure where you’re getting your information, but good topic.