Since iOS 15, SwiftUI has both a search field and a searchable view modifier to make any content view searchable.
Last updated: Oct 17, 2022
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:
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)
}
}
Placeholder Text
By default, the search field shows a localized search prompt:
You can override this by adding a prompt to the searchable view modifier:
.searchable(text: $query, prompt: "Search countries")
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))
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")
]