Empty List placeholder. SwiftUI

Year twenty twenty-one. Almost a month has passed since the WWDC. As usual Apple presented many amazing features/updates 😍. As expected updated SwiftUI framework. But did not add placeholder for List view 😭. It's not big deal, but it was one of my expectations from conference. Okay let's do it ourselves 💪

The List is one of the most used view in apps.
When using the List, devs also must handle the state of an empty data range and show a placeholder.

As an example, consider a simple list of countries. Show placeholder when data is empty.

Country model:

struct Country: Identifiable {
    let id = UUID()
    let name: String
}

So, have any ideas on how to implement a placeholder?

First idea If else

The first thing that comes to mind it's if else conditional statement.

struct ContentView: View {

    @State var countries: [Country] = [] // Data source

    var body: some View {
        if countries.isEmpty {
            Text("No Countries") // Placeholder
                .font(.largeTitle)
        } else {
            List(countries) { country in // List countires
                Text(country.name)
                    .font(.title)
            }
        }
    }
}

Advantages:

  • the most simple and clear way
  • easy to modify
  • it works and shows the placeholder when needed
  • easy to use any view for placeholder

Disadvantages:

  • the code looks cumbersome
  • not reusable

It works and sometimes it's enough. But in production, it would be nice to have a component that implements the logic of displaying a placeholder inside the component. So, goes to the next idea.

Second idea EmptyList

Improve if else idea and move logic show/hide placeholder to custom view, call it EmptyList:

struct EmptyList<Items: RandomAccessCollection, ListRowView: View, PlaceholderView: View>: View where Items.Element: Identifiable {

    private let items: Items
    private let listRowView: (Items.Element) -> ListRowView
    private let placeholderView: () -> PlaceholderView

    /// - Parameters:
    ///   - items: Source data for List. Item must implement Identifiable protocol
    ///   - listRowView: View displayed for each source Item
    ///   - placeholderView: Placeholder. View displayed when the items collection isEmpty
    init(_ items: Items,
         @ViewBuilder listRowView: @escaping (Items.Element) -> ListRowView,
         @ViewBuilder placeholderView: @escaping () -> PlaceholderView) {
        self.items = items
        self.listRowView = listRowView
        self.placeholderView = placeholderView
    }

    var body: some View {
        if !items.isEmpty {
            List { // List countires
                ForEach(items) { item in
                    self.listRowView(item)
                }
            }
        } else {
            placeholderView()
        }
    }
}

Using the EmptyList is very easy. First parameter - data source, second parameter - list row view, and finally third parameter - placeholder view.

struct ContentView: View {

    @State var countries: [Country] = [] // Data source

    var body: some View {
        EmptyList(countries, // Data items 
        listRowView: { country in // List row view
            Text(country.name)
                .font(.title)
        }, placeholderView: {
            Text("No Countries") // Placeholder
                .font(.largeTitle)
        })
    }
}

Advantages:

  • code looks clean and clear 😍
  • easy to modify custom view
  • reusable in project
  • use any view for placeholder

Disadvantages:

  • list is embedded in EmptyList view, and if want to add some ViewModifier-s to the list, need for more efforts and modify code

Usually, I would have to say that this is all and say goodbye but is not all 😎. I want to share an idea of how I cook placeholder for lists in my projects.

Preferred idea ViewModifier

Create custom ViewModifier to manage placeholder, call it EmptyDataModifier:

struct EmptyDataModifier<Placeholder: View>: ViewModifier {

    let items: [Any]
    let placeholder: Placeholder

    @ViewBuilder
    func body(content: Content) -> some View {
        if !items.isEmpty {
            content
        } else {
            placeholder
        }
    }
}

Uses EmptyDataModifier:

struct ContentView: View {

    @State var countries: [Country] = [] // Data source

    var body: some View {
        List(countries) { country in
            Text(country.name)
                .font(.title)
        }
        .modifier(EmptyDataModifier(
            items: countries,
            placeholder: Text("No Countries").font(.title)) // Placeholder
        ) 
    }
}

That's it! Also via extension can little bit improve the solution and limited apply EmptyDataModifier only for List.

extension List {

    func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
        modifier(EmptyDataModifier(items: items, placeholder: placeholder))
    }
}
struct ContentView: View {

    @State var countries: [Country] = [] // Data source

    var body: some View {
        List(countries) { country in
            Text(country.name)
                .font(.title)
        }
        .emptyListPlaceholder(
            countries,
            AnyView(ListPlaceholderView()) // Placeholder 
        )
    }
}

Advantages:

  • code look clean and clear 😍 😍 😍
  • no need to create a custom List view
  • easy to modify
  • reusable in project
  • use any view for placeholder
  • this way for can be used for any view placeholder

Disadvantages:

  • no (subjective opinion)

Instead of summary

In my opinion, the most suitable way to implement a placeholder is to use a custom ViewModifier.
I'm sure sooner or later the Apple will add a placeholder for the List view. Maybe this article will be as a request for this feature for Apple. Who knows.

Thanks for reading! See you soon.

21