21
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?
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.
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.
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)
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