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:
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:
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:
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:
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:
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:
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:
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:
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()
, orsearch()
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
andattributedText
for VoiceOver support. - Test States: Verify empty, loading, and error states in various scenarios.
Troubleshooting
- Configuration Not Showing: Ensure
contentUnavailableConfiguration
is set afterviewDidLoad
. - Button Actions Failing: Use
[weak self]
in closures to avoid retain cycles. - Layout Issues: Adjust
margins
orpadding
for proper alignment. - Dynamic Updates Not Triggered: Reassign
contentUnavailableConfiguration
or usesetNeedsUpdateContentUnavailableConfiguration()
. - Accessibility Problems: Test with VoiceOver to ensure text is readable.
- Performance: Avoid heavy computations in
contentUnavailableConfigurationUpdateHandler
.
Example: Comprehensive Usage
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
.