You are currently viewing How to Handle Empty States Using UIContentUnavailableConfiguration

How to Handle Empty States Using UIContentUnavailableConfiguration

⚠️ Information in this article is based on the Xcode 15 beta 1 release and may be subject to change.

In this year WWDC (WWDC23), Apple surprised developers with an unexpected improvement to UIKit. The introduction of UIContentUnavailableConfiguration aimed to simplify the process of creating empty states for view controllers.

According to Apple, UIContentUnavailableConfiguration is a composable description of an empty state and can be provided with placeholder content, such as an image or text.

Here’s an example of an empty state showcased in WWDC:

Now, let’s get into the details.

Note:

This article primarily focuses on the UIKit side of things. If you’re interested in learning how to do the same thing in SwiftUI, I recommend checking out this article by Antoine van der Lee.


Creating a UIContentUnavailableConfiguration

There are essentially 4 ways to create a UIContentUnavailableConfiguration:

  1. Creating from scratch
  2. Using the predefined loading configuration
  3. Using the predefined search configuration
  4. Using UIHostingConfiguration

1. Creating from Scratch

To start from scratch, we must first create an empty UIContentUnavailableConfiguration.

var config = UIContentUnavailableConfiguration.empty()

After that, we will have to configure the UIContentUnavailableConfiguration‘s placeholder content based on our needs:

config.image = UIImage(systemName: "applelogo")
config.text = "WWDC23 Demo"
config.secondaryText = "Code new worlds."

Lastly, simply set the configuration to the view controller’s contentUnavailableConfiguration.

self.contentUnavailableConfiguration = config

The empty state shown below should now be visible at the center of the view controller.

Configuring empty UIContentUnavailableConfiguration using Swift

You can refer to the UIContentUnavailableConfiguration documentation to find out all the other configurable content.

2. Using the Predefined Loading Configuration

If we need to show an empty state while waiting for your apps to load, then we can use the predefined loading configuration.

var config = UIContentUnavailableConfiguration.loading()
self.contentUnavailableConfiguration = config

Using the above code will yield the following empty state:

The loading UIContentUnavailableConfiguration in iOS 17

Just like an empty configuration, the loading configuration’s placeholder content is also customizable.

var config = UIContentUnavailableConfiguration.loading()

config.text = "Fetching content. Please wait..."
config.textProperties.font = .boldSystemFont(ofSize: 18)
        
self.contentUnavailableConfiguration = config

Here’s what it looks like after the above customization:

Configuring loading UIContentUnavailableConfiguration using Swift

3. Using the Predefined Search Configuration

Another very useful predefined configuration is the search configuration. We can use it when want to show an empty state for a search result:

var config = UIContentUnavailableConfiguration.search()
self.contentUnavailableConfiguration = config
The search UIContentUnavailableConfiguration in iOS 17

Similar to the loading configuration, the search configuration’s placeholder content is also customizable.

4. Using UIHostingConfiguration

Lastly, my personal favorite is to use the UIHostingConfiguration. This approach essentially enables us to create any empty state layout that we want using SwiftUI.

For example:

let config = UIHostingConfiguration {
    Text("Unknown error occurred, please [contact support](https://swiftsenpai.com).")
        .multilineTextAlignment(.center)
}

self.contentUnavailableConfiguration = config

The above code will give us the following outcome:

Using UIContentUnavailableConfiguration together with UIHostingConfiguration in Swift

Pro Tip:

To learn more about UIHostingConfiguration, I recommend checking out these articles.


Updating the View Controller’s contentUnavailableConfiguration

When it comes to updating the contentUnavailableConfiguration, Apple recommends developers to override a new update method called updateContentUnavailableConfiguration(using:).

As of now, there is no official documentation specifying when exactly this method will be called. However, based on my observation, it appears that the method gets triggered every time the view controller is loaded.

In situations where we need to manually trigger the update method, we can call the following function:

setNeedsUpdateContentUnavailableConfiguration()

A Real-Life Use Case

Based on what we have just discussed, I have created a sample app to showcase how to leverage the UIContentUnavailableConfiguration to display an empty state when the app is either loading or encounters an error.

To further enhance the interactivity of the sample app, I have also added a reload button (which is also part of the UIContentUnavailableConfiguration) in the error empty state. This reload button enables users to easily refresh the app’s content in the event of an error.

Sample app to showcase the usage of UIContentUnavailableConfiguration in iOS 17

Here’s the full sample code if you’re interested. Be sure to run it on Xcode 15 beta 1 or later.

import UIKit

class ContentUnavailableViewController: UIViewController {
    
    /// Variable to keep track of content fetching state
    /// nil means content is not yet fetched
    var fetchContentSuccessful: Bool? = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Create fetch button
        let fetchAction = UIAction(handler: { [weak self]_ in
            self?.startFetchCotent()
        })
        let fetchButton = UIBarButtonItem(title: "Fetch", primaryAction: fetchAction)
        navigationItem.rightBarButtonItem = fetchButton
    }
    
    override func updateContentUnavailableConfiguration(
        using state: UIContentUnavailableConfigurationState
    ) {
        // Remove existing configuration (if exist)
        contentUnavailableConfiguration = nil
        
        guard let fetchContentSuccessful else {
            // User have not trigger fetch
            return
        }
        
        if fetchContentSuccessful {
            // Prompt alert view
            showContent()
        } else {
            // Show empty state
            showError()
        }
    }
    
    /// Start the fetch content flow
    private func startFetchCotent() {
        
        Task { [weak self] in
           
            guard let self = self else { return }
            
            showLoading()
            fetchContentSuccessful = await fetchContent()
            
            // Update UI
            setNeedsUpdateContentUnavailableConfiguration()
        }
    }
    
    /// Action to run after successfully fetch content
    private func showContent() {
        
        let alert = UIAlertController(
            title: "🎉🎉🎉",
            message: "Fetch successful!",
            preferredStyle: .alert
        )
        
        let positiveAction = UIAlertAction(
            title: "OK",
            style: .default
        )
        alert.addAction(positiveAction)
        
        present(alert, animated: true)
    }
    
    /// Show the loading empty state
    private func showLoading() {
        
        var config = UIContentUnavailableConfiguration.loading()
        config.text = "Fetching content. Please wait..."
        config.textProperties.font = .boldSystemFont(ofSize: 18)
        
        self.contentUnavailableConfiguration = config
    }
    
    /// Show the empty state when encounter error
    private func showError() {
        
        var errorConfig = UIContentUnavailableConfiguration.empty()
        errorConfig.image = UIImage(systemName: "exclamationmark.circle.fill")
        errorConfig.text = "Something went wrong."
        errorConfig.secondaryText = "Please try again later."
        
        // Create configuration for reload button
        var retryButtonConfig = UIButton.Configuration.borderless()
        retryButtonConfig.image = UIImage(systemName: "arrow.clockwise.circle.fill")
        errorConfig.button = retryButtonConfig
        
        // Define the reload button action
        errorConfig.buttonProperties.primaryAction = UIAction.init(handler: { _ in
            
            Task { [weak self] in
                guard let self = self else { return }
                startFetchCotent()
            }
        })
        
        contentUnavailableConfiguration = errorConfig
    }
    
    /// A dummy function to simulate the fetch content action
    private func fetchContent() async -> Bool {
        
        // Sleep for 1 minutes to simulate a slow API call
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        
        return Bool.random()
    }
}

Wrapping Up

I really like this improvement in UIKit, it addresses an aspect that has long been neglected by developers — the empty state.

By reducing the friction associated with handling empty states, Apple effectively eliminates one of the common excuses developers have had for neglecting this aspect of app design, encouraging developers to take responsibility for ensuring that their app’s UIs are not left in a bleak, unhandled state.


If you enjoy reading this article, feel free to check out my other iOS development related articles. You can also follow me on Twitter and LinkedIn, and subscribe to my newsletter so that you won’t miss out on any of my upcoming iOS development-related articles.

Thanks for reading. 👨🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.