[go: up one dir, main page]

DEV Community

Wesley de Groot
Wesley de Groot

Posted on • Originally published at wesleydegroot.nl on

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
                        }
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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"]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Download the full example

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

  1. Consistency : By reusing a standard component, you ensure a consistent look and feel throughout your app.
  2. Localization : The view automatically adapts to the languages your app supports.
  3. 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

Top comments (0)