← 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. Most need a "no results for your search" screen too.

So teams hand-roll both. A VStack, an SF Symbol, a title, a subtitle. Then 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

  • Looks like Mail and Files for free. Spacing, dimming, and Dynamic Type are all handled
  • ContentUnavailableView.search(text:) is a ready-made "No results for '…'" state, with 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 hand it. It has no idea whether a search is actually active, or whether your data just hasn't loaded yet. Gate it behind your own emptiness check on the filtered results, the way the example above does, or it'll flash on screen for a fresh, unsearched list before the real content arrives.

It also fills whatever space it's placed in, via an internal .frame(maxWidth:maxHeight:). Nest it in a ScrollView, or drop it in as a List row, and the layout comes out odd and oversized. Place it as a direct sibling of your content inside an if/Group. Not inside scrollable content.

swiftuiios17empty-states

Related shorts