WWDC 21
Craft search experiences in SwiftUI
This year, SwiftUI adds a .searchable
view modifier which adds a search UI appropriate for the current platform
Here's how it's used in the new Weather app written in SwiftUI:
NavigationView { WeatherList(text: $text) { ForEach(data) { item in WeatherCell(item) } } } .searchable(text: $text)
What happens behind the scenes is that the .searchable
modifier puts the search field configuration into the Environment, and the views within the hierarchy look for the search field configuration there and apply it in the best way for the given platform
In this case, the NavigationView
knows how to display the search field and will show it e.g. at the top of one of its panes on iOS, or on the right side of the window toolbar on macOS
If no view knows how to display the search field, a default rendering is used which puts it in the toolbar
A search field isn't very useful without another piece of UI that displays the search results, which is something that you need to implement yourself
The Weather app does it this way:
struct WeatherList: View { @Binding var text: String @Environment(\.isSearching) private var isSearching: Bool var body: some View { WeatherCitiesList() .overlay { if isSearching && !text.isEmpty { WeatherSearchResults() } } } }
- the
.searchable
modifier puts an\.isSearching
key in the Environment, which you can check to see if a search field is focused somewhere on the screen, and show a different UI with search results if it is - the view checks the
text
property (which you need to pass yourself) and if it's not empty, displays a list of matching locations - the results list is displayed as an overlay on top of the standard UI – this is done so that when the user leaves the search field and the results list is hidden, the main UI below remains unchanged (unless they user has picked something from the results)
The second example is the "Colors" app:
NavigationView { Sidebar() DetailView() } .searchable(text: $text)
Here's how we implement search here:
- when you apply
.searchable
to a two-paneNavigationView
, it will add the search field to its first pane (the master view); if you want it to be added to the detail view, add the modifier to the detail view directly - on iOS and iPad OS, the
Sidebar
view uses the\.isSearching
Environment key to display search results as an overlay over the list sidebar - on macOS,
NavigationView
puts the search field in the right side of the sidebar, automatically collapsing it into a button if the window is too small - on the Mac we display the search results instead in the main window pane, on top of the
DetailView
- on tvOS, we use a slightly different layout structure, adding a tab bar with one tab used for the standard navigation and the other for search, and putting the search UI in the second tab above a results list:
NavigationView { TabView { Sidebar() ColorsSearch() .searchable(text: $text) } }
In most cases, the .searchable
modifier applied to NavigationView
will automatically render the search field in the appropriate place of the hierarchy
However, you always need to decide how and where to display the search results in a way appropriate for your app (which may vary between platforms)
In some cases you may want to use a different layout between platforms, as we do here in case of tvOS
Search suggestions
You can provide search suggestions for the search UI, which are examples of search queries that may be autocompleted into the field when the user picks them; suggestions give the user an idea of the types of things they can search for
Search suggestions also render in a platform-appropriate way:
- as a menu popover below the field on macOS
- as a complete search results screen on iOS
- as a button that opens the list on watchOS
Suggestions are provided as an optional view builder closure that returns a set of buttons:
.searchable(text: $text) { ForEach(suggestions) { suggestion in Button { text = suggestion.text } label: { ColorsSuggestionLabel(suggestion) } } }
There is also a shorthand version available, using the .searchCompletion
modifier
The modifier should be applied to a non-interactive element like a Label
, and it transforms it into a button which:
- updates the search text, like the button above
- dismisses the suggestions view
.searchable(text: $text) { ForEach(suggestions) { suggestion in ColorsSuggestionLabel(suggestion) .searchCompletion(suggestion.text) } }
If you want to run a search query only when the user submits the search field, not live while they're typing (e.g. because it requires a request to a server), you can use the new .onSubmit
modifier:
.searchable(text: $text) { ForEach ... } .onSubmit(of: .search) { fetchResults() }