← All shorts
04iOS 17+2 min read

ContentUnavailableView: The Empty State You Don't Have to Build

ContentUnavailableView gives every app the same polished empty state and no-search-results screen for free.

Every app eventually needs a "nothing here yet" screen and a "no results for your search" screen, and most teams hand-roll both with a VStack of an SF Symbol, a title, and a subtitle — then let the spacing and styling quietly drift between screens.

#The old way

struct EmptyBookmarksView: View {
    var body: some View {
        VStack(spacing: 12) {
            Image(systemName: "bookmark.slash")
                .font(.system(size: 48))
                .foregroundStyle(.secondary)
            Text("No Bookmarks")
                .font(.headline)
            Text("Save articles to find them here later.")
                .font(.subheadline)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

#The new way

import SwiftUI

struct BookmarksView: View {
    let bookmarks: [String]
    @State private var searchText = ""

    var filtered: [String] {
        guard !searchText.isEmpty else { return bookmarks }
        return bookmarks.filter { $0.localizedCaseInsensitiveContains(searchText) }
    }

    var body: some View {
        Group {
            if bookmarks.isEmpty {
                ContentUnavailableView(
                    "No Bookmarks",
                    systemImage: "bookmark.slash",
                    description: Text("Save articles to find them here later.")
                )
            } else if filtered.isEmpty {
                ContentUnavailableView.search(text: searchText)
            } else {
                List(filtered, id: \.self) { Text($0) }
            }
        }
        .searchable(text: $searchText)
    }
}

#Why it matters

  • Matches system apps like Mail and Files visually for free — spacing, dimming, and Dynamic Type all handled
  • ContentUnavailableView.search(text:) is a ready-made "No results for '…'" state, no string interpolation to get wrong
  • One type covers both a fully custom empty state and the standard search-empty case
  • Icon and text styling adapt automatically across size classes and platforms

#Gotcha

ContentUnavailableView.search(text:) only formats the message around the text you give it — it has no idea whether a search is actually active or whether your data just hasn't loaded yet. You still have to gate it behind your own emptiness check on the filtered results, as in the example above, or it'll flash on screen for a fresh, unsearched list before real content arrives. It also fills whatever space it's placed in via an internal .frame(maxWidth:maxHeight:), so nesting it inside a ScrollView or as a List row produces odd, oversized layout — place it as a direct sibling of your content inside an if/Group, not inside scrollable content.

swiftuiios17empty-states

Related shorts