ContentUnavailableView for Empty States

iOS 17 introduced ContentUnavailableView for showing empty states, errors, and search results. Here's how to use it effectively.
Follow along with the code: iOS-Practice on GitHub
Basic Usage
The simplest form takes a title and SF Symbol:
ContentUnavailableView("No Users", systemImage: "person.slash")
This renders a centered, styled view with an icon and text.
Full Customization
For more control, use the full initializer:
ContentUnavailableView {
Label("Error", systemImage: "exclamationmark.triangle")
} description: {
Text(error.localizedDescription)
} actions: {
Button("Retry") {
Task { await loadUsers() }
}
.buttonStyle(.borderedProminent)
}
Label - The main title with icon Description - Supporting text explaining the state Actions - Buttons for user actions (retry, create new, etc.)
Common Patterns
Empty Search Results
ContentUnavailableView.search(text: searchText)
Built-in convenience for "No results for X" with a magnifying glass icon.
Empty List
ContentUnavailableView("No Items", systemImage: "tray")
Error State with Retry
ContentUnavailableView {
Label("Connection Error", systemImage: "wifi.slash")
} description: {
Text("Check your internet connection and try again.")
} actions: {
Button("Retry", action: retry)
.buttonStyle(.borderedProminent)
}
Permission Required
ContentUnavailableView {
Label("Photos Access Required", systemImage: "photo.on.rectangle.angled")
} description: {
Text("Allow access to your photos to continue.")
} actions: {
Button("Open Settings") {
// Open app settings
}
}
Integration with Lists
Use .overlay to show ContentUnavailableView over an empty list:
List(items) { item in
ItemRow(item: item)
}
.overlay {
if items.isEmpty && !isLoading {
ContentUnavailableView("No Items", systemImage: "tray")
}
}
Or use conditional rendering:
if items.isEmpty {
ContentUnavailableView("No Items", systemImage: "tray")
} else {
List(items) { item in
ItemRow(item: item)
}
}
Search Integration
Combine with .searchable for filtered results:
@State private var searchText = ""
var filteredItems: [Item] {
if searchText.isEmpty { return items }
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
List(filteredItems) { item in
ItemRow(item: item)
}
.searchable(text: $searchText)
.overlay {
if filteredItems.isEmpty && !searchText.isEmpty {
ContentUnavailableView.search(text: searchText)
}
}
}
Before iOS 17
If supporting older versions, create a similar view:
struct EmptyStateView: View {
let title: String
let systemImage: String
var description: String?
var body: some View {
VStack(spacing: 12) {
Image(systemName: systemImage)
.font(.system(size: 48))
.foregroundColor(.secondary)
Text(title)
.font(.headline)
if let description {
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
Interview Tip
ContentUnavailableView is the modern, consistent way to handle empty states. Mentioning it shows you're current with SwiftUI best practices.