ContentUnavailableView
In the ever-evolving world of SwiftUI, creating a seamless user experience involves handling empty states gracefully. Whether it's due to networking failures or empty search results, providing clear feedback to users is crucial. Enter ContentUnavailableView, a powerful SwiftUI component introduced in iOS 17 during WWDC 2023. In this article, we'll explore how to use it effectively and why it's a valuable addition to your toolkit.
What is ContentUnavailableView
?
ContentUnavailableView is a specialized SwiftUI view designed specifically for presenting empty states. It's your go-to solution when you need to communicate to users that content is unavailable due to various reasons. By leveraging this view, you can maintain consistency across your app while ensuring accessibility and localization.
Use Case: Search Empty State
In this example, if there are no search results while the query is non-empty, we display the ContentUnavailableView. The resulting view looks familiar to users, as it's widely used across the system. Plus, it automatically translates into the languages your app supports.
struct ContentView: View {
@Bindable
var pokemon: PokemonViewModel
var body: some View {
NavigationStack {
List(pokemon.pokemon, id: \.self) { pokemon in
Text(verbatim: pokemon)
}
.navigationTitle("Pokémon")
.overlay {
if pokemon.pokemon.isEmpty {
ContentUnavailableView.search(text: pokemon.query)
}
}
.searchable(text: $pokemon.query)
}
}
}
Use Case: No Internet Connection
NetworkMonitor.swift
import SwiftUI
import Network
final class NetworkMonitor: ObservableObject {
@Published
var isConnected = true
let monitor = NWPathMonitor()
init() {
monitor.pathUpdateHandler = { [weak self] path in
if path.status == .satisfied {
// Internet connection is available
self?.isConnected = true
} else {
// Internet connection is not available
self?.isConnected = false
}
}
// Start monitoring
let queue = DispatchQueue(label: "Monitor")
monitor.start(queue: queue)
}
}
import SwiftUI
struct ContentView: View {
@ObservedObject
private var network = NetworkMonitor()
@ObservedObject
var pokemon: PokemonViewModel
var body: some View {
NavigationStack {
List(pokemon.pokemon, id: \.self) { pokemon in
Text(verbatim: pokemon)
}
.navigationTitle("Pokémon")
.overlay {
if !network.isConnected {
ContentUnavailableView {
Label(
"Connection issue",
systemImage: "wifi.slash"
)
} description: {
Text("Check your internet connection")
} actions: {
Button("Refresh") {
pokemon.fetch()
}
}
}
}
}
}
}
Combined Use Case: No Internet Connection and Empty Search Results
NetworkMonitor.swift
import SwiftUI
import Network
final class NetworkMonitor: ObservableObject {
@Published
var isConnected = true
let monitor = NWPathMonitor()
init() {
monitor.pathUpdateHandler = { [weak self] path in
if path.status == .satisfied {
// Internet connection is available
self?.isConnected = true
} else {
// Internet connection is not available
self?.isConnected = false
}
}
// Start monitoring
let queue = DispatchQueue(label: "Monitor")
monitor.start(queue: queue)
}
}
PokemonViewModel.swift
import SwiftUI
class PokemonViewModel: ObservableObject {
@Published
var query = "" {
didSet {
fetch()
}
}
@Published
var pokemon: [String] = ["Dragonite", "Pikachu", "Lugia"]
func fetch() {
if query.isEmpty {
pokemon = ["Dragonite", "Pikachu", "Lugia"]
return
}
if query.lowercased().hasPrefix("d") {
pokemon = ["Dragonite"]
}
if query.lowercased().hasPrefix("p") {
pokemon = ["Pikachu"]
}
if query.lowercased().hasPrefix("l") {
pokemon = ["Lugia"]
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
@ObservedObject
private var network = NetworkMonitor()
@ObservedObject
var pokemon = PokemonViewModel()
var body: some View {
NavigationStack {
List(pokemon.pokemon, id: \.self) { pokemon in
Text(verbatim: pokemon)
}
.navigationTitle("Pokémon")
.overlay {
if !network.isConnected {
ContentUnavailableView {
Label(
"Connection issue",
systemImage: "wifi.slash"
)
} description: {
Text("Check your internet connection")
} actions: {
Button("Refresh") {
pokemon.fetch()
}
}
}
if network.isConnected && pokemon.pokemon.isEmpty && !pokemon.query.isEmpty {
ContentUnavailableView.search(text: pokemon.query)
}
}
.searchable(text: $pokemon.query)
}
}
}
Caveats
While ContentUnavailableView is a powerful tool, it's not suitable for all scenarios. especially when handling empty states or situations where your app’s content cannot be displayed, and in complex situations a custom view may be better for empty state handling.
Benefits of Using ContentUnavailableView
- Consistency: By reusing a standard component, you ensure a consistent look and feel throughout your app.
- Localization: The view automatically adapts to the languages your app supports.
- SwiftUI Integration: It seamlessly integrates with SwiftUI's navigation stack and other components.
Conclusion
ContentUnavailableView is a valuable addition to your SwiftUI toolkit. It's a powerful tool for handling empty states gracefully, ensuring a consistent user experience across your app. By leveraging this view, you can communicate to users that content is unavailable due to various reasons, such as networking failures or empty search results. This results in a more seamless and accessible user experience.
Resources:
https://developer.apple.com/documentation/swiftui/contentunavailableview
Read more
- Understanding assertions in Swift • 4 minutes reading time.
- Generics in Swift • 3 minutes reading time.
- SwiftUI Development with DynamicUI • 4 minutes reading time.
Share
Share Mastodon Twitter LinkedIn Facebook