SwiftUI has steadily evolved from a declarative UI framework into a platform with thoughtful, production ready UI components. One of the most underrated additions is ContentUnavailableView.
At first glance, it looks simple: a view for empty states.
But in practice, it solves a surprisingly common product problem:
Before ContentUnavailableView, most SwiftUI apps implemented these screens manually using a VStack, an icon, a title, a subtitle, and maybe a button. Every team reinvented the same UI repeatedly.
Apple noticed this pattern and standardized it.
This article explores ContentUnavailableView in depth:
ContentUnavailableView?ContentUnavailableView is a SwiftUI view introduced by Apple that provides a standardized interface for displaying empty, unavailable, or failed content states.
It is commonly used when:
Instead of building these states manually, SwiftUI provides a native component that automatically follows platform styling and behavior.
The API is intentionally simple.
Basic example:
ContentUnavailableView(
"No Favorites",
systemImage: "star.slash",
description: Text("Items you favorite will appear here.")
)Output:
This produces:
And importantly:
ContentUnavailableView MattersMany developers underestimate empty states.
But empty states are product-critical UI.
A user often sees them:
Poor empty states create confusion.
Good empty states:
Apple’s Human Interface Guidelines strongly emphasize clarity during moments where no content exists.
ContentUnavailableView helps achieve that consistency.
ContentUnavailableViewBefore iOS 17, most SwiftUI code looked something like this:
VStack(spacing: 16) {
Image(systemName: "tray")
.font(.system(size: 50))
Text("No Documents")
.font(.title2)
Text("Your documents will appear here.")
.foregroundStyle(.secondary)
}
.multilineTextAlignment(.center)
.padding()Output:
This works.
But every app implemented:
The result:
ContentUnavailableView solves this by providing a canonical system component.
ContentUnavailableView is available on:
Because it is relatively new, many production apps still support earlier OS versions.
That means you may still need fallback implementations for older deployments.
Example:
if #available(iOS 17, *) {
ContentUnavailableView(
"No Notes",
systemImage: "note.text"
)
} else {
LegacyEmptyStateView()
}The most common initializer is:
ContentUnavailableView(
"No Messages",
systemImage: "message",
description: Text("New messages will appear here.")
)Output:
Parameters:
SwiftUI handles:
Conceptually, ContentUnavailableView contains:
Icon
Title
Description
Optional ActionsThis structure matters because it encourages good UX design.
Apple intentionally limits complexity here.
An empty state should communicate:
Not become a fully custom screen full of unrelated UI.
struct FavoritesView: View {
let favorites: [String] = []
var body: some View {
if favorites.isEmpty {
ContentUnavailableView(
"No Favorites Yet",
systemImage: "star",
description: Text(
"Items you mark as favorites will appear here."
)
)
} else {
List(favorites, id: \.self) { item in
Text(item)
}
}
}
}Output:
This gives you:
And the code communicates intent immediately.
That matters in large SwiftUI codebases.
One of the best features is the built-in search initializer.
ContentUnavailableView.searchThis produces Apple’s standard “No Results” search experience.
Example:
struct SearchView: View {
@State private var query = ""
var filteredItems: [String] {
items.filter {
query.isEmpty || $0.localizedCaseInsensitiveContains(query)
}
}
let items = [
"Swift",
"SwiftUI",
"UIKit",
"Combine"
]
var body: some View {
NavigationStack {
Group {
if filteredItems.isEmpty {
ContentUnavailableView.search
} else {
List(filteredItems, id: \.self) { item in
Text(item)
}
}
}
.searchable(text: $query)
.navigationTitle("Frameworks")
}
}
}Output:
This is a subtle but important API.
Most apps build search empty states inconsistently.
Apple now provides:
Users instantly understand the UI because system apps behave similarly.
This is one of SwiftUI’s biggest philosophical strengths:
common UI patterns become declarative primitives.
You can also customize search-specific states.
ContentUnavailableView {
Label("No Results", systemImage: "magnifyingglass")
} description: {
Text("We couldn't find anything matching your search.")
} actions: {
Button("Clear Search") {
query = ""
}
}Output:
Actions are where ContentUnavailableView becomes genuinely useful.
Empty states should often guide recovery.
Example:
ContentUnavailableView {
Label("No Downloads", systemImage: "arrow.down.circle")
} description: {
Text("Downloaded files will appear here.")
} actions: {
Button("Browse Files") {
openFileBrowser()
}
}Output:
This transforms the UI from passive information into actionable UX.
The most flexible initializer uses view builders.
ContentUnavailableView {
VStack {
Image(systemName: "wifi.slash")
.font(.largeTitle)
Text("No Internet Connection")
.font(.title2)
}
} description: {
Text("Please check your connection and try again.")
} actions: {
Button("Retry") {
retryRequest()
}
.buttonStyle(.borderedProminent)
}Output:
This allows:
But there’s an important design consideration.
Over-customizing empty states often hurts consistency.
Many apps misuse empty states as miniature marketing pages.
Apple’s API nudges developers toward restraint.
That’s a good thing.
struct ErrorView: View {
var retry: () -> Void
var body: some View {
ContentUnavailableView {
Label(
"Unable to Load Data",
systemImage: "wifi.exclamationmark"
)
} description: {
Text(
"The request failed. Check your connection and try again."
)
} actions: {
Button("Retry") {
retry()
}
.buttonStyle(.borderedProminent)
}
}
}Output:
ContentUnavailableView {
Label("No Projects Yet", systemImage: "folder")
} description: {
Text("Create your first project to get started.")
} actions: {
Button("Create Project") {
createProject()
}
}Output:
This pattern is extremely common in productivity apps.
ContentUnavailableView {
Label("No Invoices", systemImage: "doc.text")
} description: {
Text("Invoices you create will appear here.")
} actions: {
Button("Create Invoice") {
showInvoiceCreator()
}
}Output:
ContentUnavailableViewYou generally do not style this view aggressively.
That’s intentional.
Still, SwiftUI allows some customization.
Example:
ContentUnavailableView(
"No Notifications",
systemImage: "bell.slash",
description: Text("You're all caught up.")
)
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)Output:
The SF Symbol you choose matters significantly.
Good symbols:
Bad symbols create ambiguity.
Examples:
wifi.slashtraymagnifyingglassdoc.textfolder.badge.plusApple’s own apps heavily rely on semantic SF Symbols.
One major advantage of ContentUnavailableView is accessibility consistency.
It automatically supports:
However, developers can still make mistakes.
Avoid using only imagery without clear text.
Bad:
ContentUnavailableView {
Image(systemName: "wifi.slash")
}Good:
ContentUnavailableView(
"No Internet",
systemImage: "wifi.slash",
description: Text("Please reconnect and try again.")
)A powerful architecture pattern is combining ContentUnavailableView with enum-based view states.
Example:
enum LoadingState {
case loading
case loaded([String])
case empty
case failed
}Then:
switch state {
case .loading:
ProgressView()
case .loaded(let items):
List(items, id: \.self) { item in
Text(item)
}
case .empty:
ContentUnavailableView(
"No Items",
systemImage: "tray"
)
case .failed:
ContentUnavailableView(
"Something Went Wrong",
systemImage: "exclamationmark.triangle"
)
}This scales exceptionally well in production SwiftUI apps.
Some developers create overly complex empty states with:
Empty states should be lightweight.
Bad:
No DataGood:
No Saved Articles
Articles you bookmark will appear here.Specificity improves usability.
If users can fix the situation, provide actions.
Examples:
A loading state is not an empty state.
Do not show:
ContentUnavailableView("No Data")while a request is still loading.
Use:
ProgressViewinstead.
ContentUnavailableView vs Custom Empty Views| Feature | ContentUnavailableView |
Custom View |
|---|---|---|
| Fast to implement | Yes | No |
| Consistent with iOS | Yes | Depends |
| Accessibility defaults | Excellent | Manual |
| Fully customizable | Limited | Yes |
| Best for common empty states | Excellent | Sometimes excessive |
In most apps:
ContentUnavailableView,Avoid using ContentUnavailableView when:
Example:
Those deserve dedicated custom UI.
ContentUnavailableView is lightweight.
It’s just another SwiftUI view hierarchy.
In practice:
The real value is architectural clarity.
This API reflects a broader SwiftUI trend.
Apple increasingly converts common UI patterns into standardized declarative components:
ProgressViewShareLinkNavigationSplitViewContentUnavailableViewThis reduces:
And honestly, this is one of SwiftUI’s strongest ideas.
Not every UI problem needs a custom solution.
ContentUnavailableView may look like a small API addition, but it solves a real design and engineering problem elegantly.
It gives SwiftUI developers:
Most importantly, it encourages developers to treat empty states as first-class product experiences instead of forgotten placeholders.
That mindset matters.
Users spend more time in edge cases than developers expect:
Great apps handle those moments gracefully.
And in SwiftUI, ContentUnavailableView is now the standard way to do exactly that.
If you have suggestions, feel free to connect with me on X and send me a DM. If this article helped you, Buy me a coffee.