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.

7