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
UIContentUnavailableConfigurationproperties. - Integration with
UIViewControllerfor 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 = config2. 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 = config3. Search Configuration
For empty search results.
Example:
var config = UIContentUnavailableConfiguration.search()
config.text = "No Results"
config.secondaryText = "Try a different search term."
self.contentUnavailableConfiguration = configCustomization 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 = configThis 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
contentUnavailableConfigurationUpdateHandlerfor state changes. - Accessibility: Set
textandattributedTextfor VoiceOver support. - Test States: Verify empty, loading, and error states in various scenarios.
Troubleshooting
- Configuration Not Showing: Ensure
contentUnavailableConfigurationis set afterviewDidLoad. - Button Actions Failing: Use
[weak self]in closures to avoid retain cycles. - Layout Issues: Adjust
marginsorpaddingfor proper alignment. - Dynamic Updates Not Triggered: Reassign
contentUnavailableConfigurationor 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.