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.
Top comments (3)
I made the same mistake at first: Placing if/else and either show placeholder or List won't work anymore as soon as you make the list "searchable". If you start typing and nothing is found, no way of correcting your input because: no list, no search text field. So better not hide the list.
I came here on my search to find a way to set the background of the area the empty list occupies (sounds silly, but it's like that).
Awesome. Thanks for sharing with the rest of us!
Thank You)