Daily coverage of WWDC21.
A Swift by Sundell spin-off.

How SwiftUI’s List is becoming much more flexible this year

Published at 17:40 GMT, 09 Jun 2021
Written by: John Sundell

Likely one of the most used among the built-in views that SwiftUI ships with, List enables us to render a “table view-like” user interface on any of Apple’s platforms. This year, List has received a number of quite significant upgrades that makes it much more flexible and easier to customize. Let’s take a first look at what some of those new features are.

As a starting point, let’s say that we’re working on the following ArticleList view, which uses an ArticleListViewModel to render a list of articles:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List(viewModel.articles) { article in
            NavigationLink(
                destination: ArticleView(article: article),
                label: {
                    VStack(alignment: .leading) {
                        Text(article.title)
                            .font(.headline)
                        Text(article.description)
                            .foregroundColor(.secondary)
                    }
                }
            )
        }
    }
}

The above is currently written using concepts and APIs that have been available in SwiftUI since day one, but let’s now see how we can apply some of the new features to give our list a more custom style, and to make it more capable.

New shorthand syntax for applying styling protocols

Let’s start with a somewhat minor feature that’s still a very welcome change, and that’s that we can now use an enum-like shorthand syntax to refer to any of the built-in ListStyle types that SwiftUI ships with. For example, if we wanted to apply the “inset grouped” style to our list, then we no longer have to spell out the whole InsetGroupedListStyle name, but can instead simply refer to it as .insetGrouped:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List(viewModel.articles) { article in
            ...
        }
        .listStyle(.insetGrouped)
    }
}

I personally love small little conveniences like this, since they can make our code feel much more natural to both type and read.

Item bindings and custom swipe actions

Next, let’s take a look at how we can now add completely custom swipe actions to our lists. To do that in this case, let’s start by nesting a ForEach within our List (since, in the world of SwiftUI, everything that has to do with list mutations is driven by ForEach, not by List). Then, let’s use another new feature, collection element bindings to have the system automatically create a mutable binding to each element within our articles array:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List {
            ForEach($viewModel.articles) { $article in
                ...
            }
        }
        .listStyle(.insetGrouped)
    }
}

A really interesting note about the above new way of creating collection element bindings is that it automatically works even when our app is running on older operating system versions. It’s completely backward compatible!

Since each article value is now mutable within our ForEach closure, we can apply the new swipeActions modifier to each of our NavigationLink item views to implement our custom swipe action. In this case we’ll enable the user to easily favorite (or unfavorite) a given article through an action that’s revealed when swiping over an item’s view:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List {
            ForEach($viewModel.articles) { $article in
                NavigationLink(
                    ...
                )
                .swipeActions {
                    Button(
                        action: {
                            article.isFavorite.toggle()
                        },
                        label: {
                            if article.isFavorite {
                                Label("Remove from favorites",
                                    systemImage: "star.slash"
                                )
                            } else {
                                Label("Add to favorites",
                                    systemImage: "star"
                                )
                            }
                        }
                    )
                    .tint(article.isFavorite ? .red : .green)
                }
            }
        }
        .listStyle(.insetGrouped)
    }
}

Note how we’re even able to give our swipe action a custom tint color (using the new tint modifier) based on whether the action will favorite or unfavorite a given article.

Pull to refresh

Personally, pull to refresh was very high on my list of SwiftUI feature requests, so I was really happy to see that this year’s release adds built-in support for that very common UI paradigm.

Not only that, but it’s powered by async/await, which makes it possible for the system to know when a reload operation was completed without requiring any extra code on our part. All that we have to do is to apply the refreshable modifier to our list, and then use await to call our view model’s asynchronous reload method within that modifier’s closure:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List {
            ...
        }
        .listStyle(.insetGrouped)
        .refreshable {
            await viewModel.reload()
        }
    }
}

To learn more about async/await and how it can be used from within SwiftUI views in general, check out this article from yesterday, and don’t miss the truly essential “Meet async/await in Swift” WWDC session.

Since the system is automatically made aware of when our viewModel.reload() call will complete, it can prevent duplicate refresh operations from happening at the same time, and it can take care of showing and hiding the loading spinner that will be displayed while the refresh is happening.

Customizable separators

Another feature that’s been very commonly requested ever since the introduction of SwiftUI is some form of API to hide, or otherwise customize, the default separators that are rendered between each item within a list. Well, I’m happy to report that this year Apple have responded to that request, and we’re now able to use the new listRowSeparator modifier to completely hide the separators that we don’t wish to render:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List {
            ForEach($viewModel.articles) { $article in
                NavigationLink(
                    ...
                )
                .swipeActions {
                    ...
                }
                .listRowSeparator(.hidden)
            }
        }
        ...
    }
}

Since the above modifier is called on each list item view, rather than the list itself, that gives us a lot of flexibility to dynamically hide or show individual separators based on what kind of UI that we’d like to build. There’s also an equivalent API for controlling the appearance of section separators as well.

However, not only can we now hide the separators within a list, we can also tint them with a custom tint color — like this:

struct ArticleList: View {
    @ObservedObject var viewModel: ArticleListViewModel

    var body: some View {
        List {
            ForEach($viewModel.articles) { $article in
                NavigationLink(
                    ...
                )
                .swipeActions {
                    ...
                }
                .listRowSeparatorTint(.blue)
            }
        }
        ...
    }
}

Again, the above modifier is applied to each individual item view within the list, which makes it possible to tint different separators with different colors.

Conclusion

SwiftUI continues to become more flexible and more capable every year, which is not that surprising, but still incredibly good news. I’ll continue to explore more of the new SwiftUI APIs that were introduced this year, and will share my findings with you, either on this site or over on Swift by Sundell.

For more on what’s new in SwiftUI, I recommend watching the “What’s new in SwiftUI” session.

Thanks for reading!

Written by: John Sundell
RaycastRaycast

Take the macOS Spotlight experience to the next level: Create Jira issues, manage GitHub pull requests and control other tools with a few keystrokes. Automate every-day tasks with scripts and boost your developer productivity by downloading Raycast for free.