Making SwiftUI Views Searchable

SwiftUI gains a search field in iOS 15. The searchable view modifier lets you make any content view searchable reducing the need to roll your own search bar.

Searching SwiftUI Views

If you’ve ever struggled with UIKit’s UISearchController you’re probably going to like how much less effort it takes to add search to a SwiftUI view. Here’s a list of countries that I want to search:

List of countries

The implementation of the view is plain list in a navigation view:

struct WorldView: View {
  @FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
  private var countries: FetchedResults<Country>
       
  var body: some View {
    NavigationView {
      List(countries) { country in
        NavigationLink(destination: 
          CountryView(country: country)) {
          CountryCell(country: country)
        }
      }
      .listStyle(.plain)
      .navigationTitle("World")

      NoCountrySelected()
    }
  }
}

I’m using Core Data to store the country objects but that’s not a requirement (see Configuring SwiftUI Fetch requests for a recap).

The Searchable View Modifier

To get started with adding search to our list we need a state variable to store our search query text:

@State private var query = ""

Then we add the .searchable view modifier to our content view. Note that you are not limited to searching lists, you can add the modifier to any content view:

NavigationView {
}
.searchable(text: $query)

Finally, we use a change in the query text to update our fetch request to generate new results:

NavigationView { ...
}
.searchable(text: $query)
.onChange(of: query) { newValue in
  countries.nsPredicate = searchPredicate(query: newValue)
}

Since we’re using Core Data we update the predicate of the fetch request using the query text:

private func searchPredicate(query: String) -> NSPredicate? {
  if query.isBlank { return nil }
    return NSPredicate(format: "%K BEGINSWITH[cd] %@",
    #keyPath(Country.name), query)
  }
}

Search bar in navigation view

Placeholder Text

By default, the search field shows a localized search prompt:

Search placeholder text

You can override this by adding a prompt to the searchable view modifier:

.searchable(text: $query, prompt: "Search countries")

Search countries placeholder text

Search Bar Placement

The search bar appears in a position appropriate to the platform. When used with a navigation view on iOS and iPadOS it defaults to applying to the primary view of a two column split view or the second column of a triple column view. You can always override that by adding the modifier directly on the desired column.

You can also change the preferred placement of the search field within the containing view hierarchy. The default is to place the search field automatically depending on the platform. You can override the default to prefer the navigationBarDrawer, sidebar or toolbar:

.searchable(text: $query, placement: .sidebar)

Note this has no effect in my example as I don’t have a sidebar. When shown below the navigation bar, the search bar collapses under the bar when the user scrolls. You can override that to keep the search bar visible by changing the display mode:

.searchable(text: $query,
  placement: .navigationBarDrawer(displayMode: .always))

Search bar visible when scrolling

isSearching and dismissSearch

The isSearching environment variable tells you if the user is interacting with a search field:

@Environment(\.isSearching)
private var isSearching: Bool

In the WWDC session video (see below) they use this to present the search results in an overlay. I’ve also found it useful when I want to show a search scope bar when the user is searching:

if isSearching {
  ScopeBar(selected: $searchBy)
}

The dismissSearch environment property gives you a method that dismisses an active search:

@Environment(\.dismissSearch)
private var dismissSearch

Calling dismissSearch() will also set isSearching to false:

if isSearching {
  Button("Dismiss") {
    dismissSearch()
  }
}

Search Submit Action

If you only want to perform the search when the user submits the query add an onSubmit action:

.onSubmit(of: .search) {
  // perform query
}

Search Suggestions

Finally, you can provide a search suggestions view as part of the searchable view modifier:

.searchable(text: $scope.beginsWith) {
  ForEach(suggestions) { suggestion in
    Text(suggestion.title).searchCompletion(suggestion.completion)
  }
}

I’m using Text views but you can use any view. The search completion provides the text that replaces the search query when the user selects a suggestion. I’m using a static list of suggestions, but you could generate them dynamically based, for example, on search history:

struct SearchSuggestion: Identifiable {
    var id: Int
    var title: String
    var completion: String
}

private var suggestions = [
  SearchSuggestion(id: 1, title: "πŸ‡¨πŸ‡¦ Canada", completion: "Canada"),
  SearchSuggestion(id: 2, title: "πŸ‡¬πŸ‡§ UK", completion: "United Kingdom"),
  SearchSuggestion(id: 3, title: "πŸ‡ΊπŸ‡Έ USA", completion: "United States")
]

Search suggestions

Learn More