swiftyplace site icon

How to use SwiftUI List View

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.

static list example in SwiftUI
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:

SwiftUI List and Form look differently on macOS,
A Comparison of List and Form on macOS.

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:

using custom cell in SwiftUI list
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.

an example for a section with a Diclosure view in a SwiftUI list view
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.

hierarchical list in SwiftUI
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()
        }
    }
}
default list styling for SwiftUI
List styling in different contexts (simulator iPadOS 14.2)
default list styling for List styling iPadOS 16
List styling in different contexts (simulator iPadOS 16.2)

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:

SwiftUI list style examples for plain, inset and grouped
SwiftUI list style examples for sidebar and inset grouped
The sidebar style is very similar to the inset grouped list style. But it also makes the sections collapsable. Control views like buttons look different.
examples SwiftUI list styles for macOS
On macOS, the sidebar has by default a translucent background.
list style examples for macOS: inset with alternates row background
list style examples for macOS: bordered with alternates row background

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)
SwiftUI list view with custome appearance
Left is a list in inset list styling and on the right, I went a bit crazy with the styling πŸ˜‰

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
        }
    }
}
list selection for single and multiple elements

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

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.

Share:

Subscribe To My Newsletter

Table of Contents

One Response

Leave a Reply

Your email address will not be published. Required fields are marked *

More Posts