Skip to content

UIContentUnavailableConfiguration

Introduced in iOS 17, UIContentUnavailableConfiguration is a UIKit content configuration that enables developers to display empty states, loading states, or search result placeholders in a UIViewController. It simplifies handling scenarios where content is unavailable, such as empty data sets, network errors, or ongoing data fetches, with customizable text, images, and actions.

Overview

UIContentUnavailableConfiguration is part of UIKit’s content configuration system, applied via the contentUnavailableConfiguration property of UIViewController. It provides predefined configurations (empty(), loading(), search()) and supports extensive customization for text, images, buttons, and layout.

Key Features:

  • Displays centered content with a title, description, image, and optional buttons.
  • Predefined configurations for common use cases.
  • Customizable appearance using UIContentUnavailableConfiguration properties.
  • Integration with UIViewController for seamless state management.

Basic Usage

Set a configuration on a view controller’s contentUnavailableConfiguration property to display an empty state.

Example: Basic Empty State:

swift
import UIKit

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var config = UIContentUnavailableConfiguration.empty()
        config.text = "No Data Available"
        config.secondaryText = "Check back later for updates."
        self.contentUnavailableConfiguration = config
    }
}

This displays a centered view with "No Data Available" as the primary text and "Check back later for updates." as the secondary text.

Predefined Configurations

UIContentUnavailableConfiguration offers three standard configurations:

1. Empty Configuration

For scenarios with no content, such as an empty table or collection view.

Example:

swift
var config = UIContentUnavailableConfiguration.empty()
config.text = "No Items Found"
config.secondaryText = "Add items to get started."
config.image = UIImage(systemName: "tray")
self.contentUnavailableConfiguration = config

2. Loading Configuration

For indicating data is being fetched.

Example:

swift
var config = UIContentUnavailableConfiguration.loading()
config.text = "Fetching Content..."
config.textProperties.font = .boldSystemFont(ofSize: 18)
self.contentUnavailableConfiguration = config

3. Search Configuration

For empty search results.

Example:

swift
var config = UIContentUnavailableConfiguration.search()
config.text = "No Results"
config.secondaryText = "Try a different search term."
self.contentUnavailableConfiguration = config

Customization Options

UIContentUnavailableConfiguration supports extensive customization:

  • Text: text, secondaryText, attributedText, secondaryAttributedText.
  • Text Properties: textProperties, secondaryTextProperties (font, color, alignment).
  • Image: image, imageProperties (tint color, size).
  • Buttons: button, secondaryButton (title, action, style).
  • Layout: direction, axialAlignment, margins, padding.

Example: Custom Empty State with Button:

swift
var config = UIContentUnavailableConfiguration.empty()
config.text = "Error Loading Data"
config.secondaryText = "Something went wrong."
config.image = UIImage(systemName: "exclamationmark.triangle")
config.imageProperties.tintColor = .systemRed
config.button = .primary(action: UIAction(title: "Retry") { [weak self] _ in
    self?.reloadData()
})
config.buttonProperties.tintColor = .systemBlue
self.contentUnavailableConfiguration = config

This displays an error state with a red warning icon and a "Retry" button that triggers reloadData().

Updating Configurations

To update the configuration, modify the existing one or apply a new one. Call contentUnavailableConfigurationUpdateHandler to handle dynamic updates.

Example: Dynamic Updates:

swift
class MyViewController: UIViewController {
    var isLoading = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        contentUnavailableConfigurationUpdateHandler = { [weak self] config in
            guard let self else { return config }
            var updatedConfig = self.isLoading ? UIContentUnavailableConfiguration.loading() : .empty()
            if self.isLoading {
                updatedConfig.text = "Loading..."
            } else {
                updatedConfig.text = "No Data"
                updatedConfig.secondaryText = "Tap to retry."
                updatedConfig.button = .primary(action: UIAction(title: "Retry") { _ in
                    self.isLoading = true
                })
            }
            return updatedConfig
        }
    }
    
    func finishLoading() {
        isLoading = false
        contentUnavailableConfiguration = contentUnavailableConfiguration // Triggers update
    }
}

State Management

Use UIContentUnavailableConfigurationState to encapsulate custom state.

Example:

swift
class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var config = UIContentUnavailableConfiguration.empty()
        config.state["retryCount"] = 0
        config.button = .primary(action: UIAction(title: "Retry") { [weak self] _ in
            self?.incrementRetryCount()
        })
        self.contentUnavailableConfiguration = config
    }
    
    func incrementRetryCount() {
        if var config = contentUnavailableConfiguration as? UIContentUnavailableConfiguration {
            config.state["retryCount"] = (config.state["retryCount"] as? Int ?? 0) + 1
            config.secondaryText = "Retries: \(config.state["retryCount"] ?? 0)"
            self.contentUnavailableConfiguration = config
        }
    }
}

Integration with Table/Collection Views

For table or collection views, set the configuration when the data source is empty.

Example: Table View:

swift
class TableViewController: UITableViewController {
    var items: [String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateEmptyState()
    }
    
    func updateEmptyState() {
        if items.isEmpty {
            var config = UIContentUnavailableConfiguration.empty()
            config.text = "No Items"
            config.button = .primary(action: UIAction(title: "Add Item") { [weak self] _ in
                self?.items.append("New Item")
                self?.tableView.reloadData()
                self?.updateEmptyState()
            })
            contentUnavailableConfiguration = config
        } else {
            contentUnavailableConfiguration = nil // Hide empty state
        }
    }
}

Best Practices

  • Use Predefined Configurations: Start with empty(), loading(), or search() for consistency.
  • Customize Sparingly: Maintain Apple’s design guidelines for user familiarity.
  • Handle Actions: Ensure buttons trigger meaningful actions (e.g., retry, refresh).
  • Dynamic Updates: Use contentUnavailableConfigurationUpdateHandler for state changes.
  • Accessibility: Set text and attributedText for VoiceOver support.
  • Test States: Verify empty, loading, and error states in various scenarios.

Troubleshooting

  • Configuration Not Showing: Ensure contentUnavailableConfiguration is set after viewDidLoad.
  • Button Actions Failing: Use [weak self] in closures to avoid retain cycles.
  • Layout Issues: Adjust margins or padding for proper alignment.
  • Dynamic Updates Not Triggered: Reassign contentUnavailableConfiguration or use setNeedsUpdateContentUnavailableConfiguration().
  • Accessibility Problems: Test with VoiceOver to ensure text is readable.
  • Performance: Avoid heavy computations in contentUnavailableConfigurationUpdateHandler.

Example: Comprehensive Usage

swift
import UIKit

class ContentViewController: UIViewController {
    enum State {
        case loading, empty, error(String), searchEmpty
    }
    
    var state: State = .loading {
        didSet { updateConfiguration() }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        updateConfiguration()
    }
    
    func updateConfiguration() {
        var config: UIContentUnavailableConfiguration
        switch state {
        case .loading:
            config = .loading()
            config.text = "Fetching Data..."
            config.textProperties.color = .systemGray
        case .empty:
            config = .empty()
            config.text = "No Content"
            config.secondaryText = "Add items to begin."
            config.button = .primary(action: UIAction(title: "Add") { [weak self] _ in
                self?.state = .loading
            })
        case .error(let message):
            config = .empty()
            config.text = "Error"
            config.secondaryText = message
            config.image = UIImage(systemName: "exclamationmark.circle")
            config.button = .primary(action: UIAction(title: "Retry") { [weak self] _ in
                self?.state = .loading
            })
        case .searchEmpty:
            config = .search()
            config.text = "No Results Found"
            config.secondaryText = "Try different keywords."
        }
        config.textProperties.font = .systemFont(ofSize: 20, weight: .medium)
        config.imageProperties.tintColor = .systemBlue
        self.contentUnavailableConfiguration = config
    }
    
    // Simulate data fetch
    func fetchData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.state = .error("Network timeout")
        }
    }
}

Additional Resources

For more details, refer to the official Apple Developer Documentation for UIContentUnavailableConfiguration.

Released under the MIT License.