Streaming is available in most browsers,
and in the Developer app.
-
Craft search experiences in SwiftUI
Discover how you can help people quickly find specific content within your apps. Learn how to use SwiftUI's .searchable modifier in conjunction with other views to best incorporate search for your app. And we'll show you how to elevate your implementation by providing search suggestions to help people understand the types of searches they can perform.
Resources
Related Videos
WWDC21
-
Download
♪ ♪ Hi, I'm Harry, an engineer on the SwiftUI team. Welcome to "Craft search experiences in SwiftUI." Search is all-new in SwiftUI this year, and I'm excited to show you how to start using these features. Sometimes, you want to find something specific within an app, but many apps contain large collections of data, which can make it difficult to find exactly what you're looking for. Whether it's finding the latest app trend, your next favorite song, or the right podcast for your morning commute, implementing search helps do just that. The best search experience will vary based on the structure and content of the app. But, in general, all experiences will include a search field that defines a search query. Now, you can implement search directly in SwiftUI. Let's take a look. In this talk, I'll introduce the new searchable modifier that forms the foundation of all search features in SwiftUI. I'll showcase how a navigation view integrates with the searchable modifier. Finally, I'll cover extending the functionality of searchable modifiers by adding search suggestions. Searchable is a new view modifier in SwiftUI available on all platforms. It allows you to mark view content as being, well, searchable. The content will define exactly what that means. Throughout this talk, I'll explore examples of this modifier on each platform. Now, to better understand this modifier, let's talk about the weather! A great example of search on iOS can be found in the new Weather app. When viewing your list of cities, you can start typing into the search bar, which will search for a new city to add to your list. To illustrate how Weather uses a searchable modifier to power this feature, let's break down the structure of their UI. Weather starts with a navigation view, which provides a navigation bar. Weather adds a custom list as the navigation view's content. And inside of that list, it adds a ForEach over its cells. Finally, Weather adds the searchable modifier to its navigation view. At the core of all searchable modifiers is the configuration of a search field. The searchable modifier takes the configured search field and passes that down, through the environment, for other views to use in the best way for each platform. Here, the navigation view understands that search field and will render it as a search bar. If no views use the configured search field, the searchable modifier provides a default implementation of rendering the search field in the toolbar. Now, a search field is often not the only piece of search-related UI. Displaying some form of search results is a common design pattern. Weather follows this pattern, and whenever it sees a non-empty search query, it switches its custom list to display another list containing the results of the current query. Let's see how Weather can use information vended by the searchable modifier to achieve this UI. Here's Weather's custom list. Searchable modifiers set up a new environment property, called isSearching, that Weather uses to dynamically change the view being displayed, based on whether a user is actively searching. Weather queries the isSearching environment property and the text of the search to conditionally show its results view. When rendering your own results, consider using an overlay so that the state of your main UI is unchanged after a user returns from their search interaction. Let's talk a little bit more about how a navigation view integrates with the new searchable modifier. To do that, I'd like to introduce an app I've been working tirelessly on. I call it Colors. This app lets users manage a library of their favorite sets of colors, called Palettes. Maybe you wanna play with different paints for your room, themes for your app, or just get help picking the color of your new M1 iMac. No matter your desire, Colors is the app for you. The basic structure of Colors is a double-column navigation view, where the first column, the root view, is a sidebar on iPadOS and macOS, or the root of a navigation stack on other platforms. In the sidebar, you can see my library of color palettes. In the detail view, you can see the currently selected color palette, as well as how those colors might appear to users with visual differences. As people started using the app, I began to notice something. People love colors. They're adding colors to their library constantly, but as a library gets large, it becomes difficult to find a specific color palette, as people have to scroll through their whole library to find it. At this point, I've decided it’s time to add search functionality. I'd like this feature to follow platform conventions so that people using my app immediately understand it. Let's take a look at how I can use the searchable modifier to implement this feature.
Here I have the navigation view I built as part of the Colors app. To implement search, I'll add the searchable modifier to my navigation view. Just like before, I'll provide it with a binding to the state backing the search query. Again, this will be rendered as a search bar on iOS and iPadOS. When a navigation view is the searchable modifier's content, it associates the search field with one of its columns. Which column it uses is dependent on the number of columns provided to the navigation view. Since I have a two-column navigation view, the search bar is associated with the sidebar column on both iOS and iPadOS. If you want the search field associated with a column other than the default, you can place the searchable modifier on your desired column, like you would with the existing toolbar modifier. For this app, I'll keep searchable on the navigation view. Just like with Weather, I'll use the isSearching environment property to dynamically display my search results over the sidebar. The same placement of the searchable modifier on macOS will render the search field placed in the most trailing position of the toolbar, with behaviors expected on that platform, like automatically collapsing as a window shrinks. Here, I'll render my search results in the detail pane of the app for a more typical experience on macOS. watchOS behaves similarly to iOS in that it, too, places a search field at the top of the view in the toolbar. Here, SwiftUI chooses the first column to associate the search field with. Notice how I didn't change where I placed the searchable modifier to produce these different behaviors across the platforms. The structure of my app didn't change across these platforms and remained a double-column navigation view. SwiftUI understands this structure and the different platform conventions, so it handled implementing them on my behalf. When I look at tvOS, I realize that the structure of my app on the other platforms could be made more appropriate. tvOS typically renders search as a tab in a tab view, but my app has no tab view. With just a few tweaks to my app, I can fix that. Instead of rendering a double-column navigation view, I can make a more typical tvOS experience by rendering a single-column navigation view with a tab view as the navigation view’s content. In my tab view, I'll place the existing sidebar view and add a new search tab. The search tab represents the placeholder view that will be visible when someone first navigates to that tab. Finally, instead of wrapping the navigation view, I'll move the searchable modifier to wrap my search tab. Once a non-empty search query has been entered, I'll transition my view to display my search results. Now I've added search functionality to each platform. Thanks to the declarative nature of SwiftUI, where the navigation structure of my app remained consistent, I was able to rely on SwiftUI, and let the implementation of the searchable modifier pick the appropriate interface for that particular navigation structure. On tvOS, where the structure of my app changed, I took what I learned about the searchable modifier and applied that to the different structure. Only what I defined as "searchable" changed with that structure. Now that you have an understanding of the searchable modifier, let's move on to our final topic: search suggestions. After using search in my app, a few users have reported that while they really like the new functionality search provides, they're sometimes at a loss for what they're able to search for. Many apps use search suggestions to help guide people towards the type of search queries they can provide. These suggestions represent completed search queries that may be presented in a menu, like on macOS, in a list, like on iOS, or as button that presents a list, like on watchOS. Suggestions give people an idea for the types of things they can search for. SwiftUI offers an easy way to add search suggestions to your apps. Let's take a look. Looking at my app, you can see that I've configured a searchable modifier with text. Searchable modifiers offer an optional parameter, called suggestions, that I can start including. For the suggestions parameter, I'll provide a view. This might just be a few static buttons or, more likely, it will be a ForEach over a dynamic set of suggestions coming from my apps database or from a server. SwiftUI looks at this view and will present it based on whether there are any suggestions to display. For example, watchOS will render an icon in its search field when you provide non-empty suggestions. A common pattern here might be to provide a ForEach of buttons that, when interacted with, updates the text binding provided to the searchable modifier with the search suggestion’s textual value. In fact, we expect this pattern to be so common that we've added a searchCompletion modifier that does just this. You can use the searchCompletion modifier with non-interactive views. It will convert that view into a button that updates the search text and dismisses the currently presented suggestion. If you're creating an app where users first interact with suggestions to then fetch a complete set of search results based on that suggestion, consider using the new onSubmit modifier to know when to fetch your search results. Pass in a value of search to the onSubmit modifier, and the closure you provide will be invoked whenever the user submits their search query. This typically occurs when they select a search suggestion or press Enter on a hardware keyboard. You can also use the new onSubmit modifier in conjunction with text fields or secure fields for non-search-related submissions. Using the suggestions parameter along with the search completion modifier provides an easy way to add powerful search-suggestion functionality to your app. I hope you've enjoyed a brief tour of some of the search functionality that SwiftUI now offers. In summary, a searchable modifier allows you to describe view content as searchable. Navigation views integrate with the searchable modifier to provide platform-appropriate experiences based on the content of the navigation view. Use the environment's isSearching property to dynamically adjust the UI of your app when a user is searching. Use the search completion modifier and the suggestions parameter of the searchable modifier to add search suggestions to your app. Now, go out there and start adding search to all your SwiftUI apps, and have a great WW. [upbeat music]
-
-
0:10 - Colors Suggestions
struct ColorsContentView: View { @State var text = "" var body: some View { NavigationView { Sidebar() DetailView() } .searchable(text: $text) { ForEach(suggestions) { suggestion in Button { text = suggestion.text } label: { ColorsSuggestionLabel(suggestion) } } } } }
-
1:17 - New Searchable Modifier
ContentView() .searchable(text: $text)
-
1:58 - Weather Search
NavigationView { WeatherList(text: $text) { ForEach(data) { item in WeatherCell(item) } } } .searchable(text: $text)
-
3:11 - Weather List
struct WeatherList: View { @Binding var text: String @Environment(\.isSearching) private var isSearching: Bool var body: some View { WeatherCitiesList() .overlay { if isSearching && !text.isEmpty { WeatherSearchResults() } } } }
-
5:07 - Colors Search
struct ColorsContentView: View { @State var text = "" var body: some View { NavigationView { Sidebar() DetailView() } .searchable(text: $text) } }
-
7:15 - Colors Search with TV
struct ColorsContentView: View { @State var text = "" var body: some View { NavigationView { #if os(tvOS) TabView { Sidebar() ColorsSearch() .searchable(text: $text) } #else Sidebar() DetailView() #endif } #if !os(tvOS) .searchable(text: $text) #endif } }
-
9:09 - Colors Search Completions
struct ColorsContentView: View { @State var text = "" var body: some View { NavigationView { Sidebar() DetailView() } .searchable(text: $text) { ForEach(suggestions) { suggestion in ColorsSuggestionLabel(suggestion) .searchCompletion(suggestion.text) } } } }
-
10:21 - On Submit
ContentView() .searchable(text: $text) { MySearchSuggestions() } .onSubmit(of: .search) { fetchResults() }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.