In iOS 17, Apple added a content unavailable view for when you need to show an empty state. It’s available for both SwiftUI and UIKit.
ContentUnavailableView (SwiftUI)
Apple added the ContentUnavailableView
to SwiftUI in iOS 17. You use it anytime you need to show an empty state. Some common examples:
- An empty list
- The detail view of a split view with nothing selected.
- A search that did not return any results.
The ContentUnavailableView
is configurable with a label, and optionally, a description, and a set of actions:
ContentUnavailableView {
Label("No messages", systemImage: "exclamationmark.icloud")
} description: {
Text("Unable to receive new messages")
} actions: {
Button("Try again") {
store.fetch()
}
}
Empty Detail View
An example, with a split view showing a list of countries in the sidebar and a detail view that shows the selected item. With nothing selected in the sidebar we show the content unavailable view for the detail view:
NavigationSplitView {
CountryList(selection: $selectedCountry)
} detail: {
if let selectedCountry {
CountryView(country: selectedCountry)
} else {
ContentUnavailableView {
Label("No country selected.",
systemImage: "map.circle.fill")
} description: {
Text("Choose a country to see more details here.")
}
}
}
Empty Search Results
Empty search results are common enough that Apple added a specialized version of ContentUnavailableView for searchable views. Using it in a searchable country list with a SwiftData query providing the search results (some details omitted):
struct CountryList: View {
@Binding private var selection: Country.ID?
@Query(sort: [SortDescriptor(\Country.name,
comparator: .localizedStandard)])
private var countries: [Country]
init(selection: Binding<Country.ID?>,
predicate: Predicate<Country>? = nil) { ... }
var body: some View {
List(selection: $selection) {
ForEach(countries) { country in
CountryRow(country: country)
}
}
.overlay {
if countries.isEmpty {
ContentUnavailableView.search
}
}
}
}
If the collection of countries is empty I overlay the search content unavailable view over the list view:
The content view extracts the search query from the searchable view which in my case is the parent view (see making SwiftUI views searchable):
CountryList(selection: $selection, predicate: scope.predicate)
.searchable(text: $scope.beginsWith)
UIContentUnavailableConfiguration (UIKit)
It’s good to see that Apple is not keeping the new features just for SwiftUI. The content unavailable views are also usable with UIKit.
In iOS 17, UIViewController has a contentUnavailableConfiguration
property. This is an optional that you typically set to an instance of UIContentUnavailableConfiguration
when you want the view controller to show the empty state.
UIKit provides three default configurations for empty, loading, and search empty states. (I’m not sure why SwiftUI didn’t also get the loading version?).
You can set the property directly, but the Apple sample code appears to prefer calling setNeedsUpdateContentUnavailableConfiguration
anytime you update content and then override updateContentUnavailableConfiguration(using:)
in your view controller to update the configuration property.
Empty Detail View
Let’s look at the same two examples, first the empty detail view when the sidebar has no selection. My detail view controller has an optional country property which when set updates the user interface:
final class CountryViewController: UIViewController {
var country: Country? {
didSet {
configureView()
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureView()
}
}
When my view is first loaded and any time my country property changes I call configureView
to update the user interface. That’s also a good time to update my content unavailable configuration:
private func configureView() {
title = country?.name
...
if #available(iOS 17.0, *) {
setNeedsUpdateContentUnavailableConfiguration()
}
}
When the view controller calls my updateContentUnavailableConfiguration
method I check if I have a country and if not set an empty content configuration:
@available(iOS 17.0, *)
override func updateContentUnavailableConfiguration(using state:
UIContentUnavailableConfigurationState) {
var config: UIContentUnavailableConfiguration?
if country == nil {
var empty = UIContentUnavailableConfiguration.empty()
empty.background.backgroundColor = .systemBackground
empty.image = UIImage(systemName: "map.circle.fill")
empty.text = "No country selected"
empty.secondaryText = "Choose a country to see more details here"
config = empty
}
contentUnavailableConfiguration = config
}
The content configuration overlays the normal view so make sure to set the background if you don’t want to see the rest of the user interface.
Note that if I do have a country to show the contentUnavailableConfiguration property is nil
removing any content unavailable view.
Empty Search Results
I’m driving my search results from a fetched results controller. The controllerDidChangeContent
delegate method gives me a convenient point to trigger an update of the content unavailable configuration:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if #available(iOS 17, *) {
setNeedsUpdateContentUnavailableConfiguration()
}
}
Then overriding the updateContentUnavailableConfiguration
method I check if I have any fetched objects and if not set the default search configuration:
var fetchedObjectCount: Int {
fetchedResultsController.fetchedObjects?.count ?? 0
}
@available(iOS 17.0, *)
override func updateContentUnavailableConfiguration(using state: UIContentUnavailableConfigurationState) {
var config: UIContentUnavailableConfiguration?
if fetchedObjectCount == 0 {
config = .search()
}
contentUnavailableConfiguration = config
}