Search functionality is a crucial feature of any app, and SwiftUI provides developers with powerful tools to create effective search experiences. One such tool is Search Scopes, which allows users to refine their search results by selecting specific categories or filters. By using Search Scopes in your SwiftUI app, you can provide a more targeted and personalized search experience, improving the user’s ability to find the content they need quickly and efficiently. In this blog post, we’ll explore how to use Search Scopes in SwiftUI to enhance search functionality on iOS and macOS.
Search Scope is available for iOS 16+ and macOS 13+.
This blog is an addition to my previous post about SwiftUI Search Bar: Best Practices and Examples. Please have a look there if you need an overview of the SwiftUI search.
Defining a Search Scope
As an example, I will continue using a Meal app that shows a list of meals. The Meal data is defined as follows:
struct Meal: Codable, Hashable, Identifiable {
let imageURL: String
let id: String
let category: MealCategory
let name: String
let location: String
let rating: Double
var tags: [String]
}
The category is defined as follows:
enum MealCategory: String, Codable, CaseIterable, Identifiable, Hashable {
case african = "African"
case american = "American"
case asian = "Asian"
case europian = "Europian"
case oceanian = "Oceanian"
var id: String { rawValue }
}
When searching for a meal, my users can currently enter a search text in the search bar. I then use that to filter the meals that have this search term in the name:
struct MealListView: View {
@StateObject var viewModel = MealListViewModel()
@State var selectedMeal: Meal? = nil
var body: some View {
NavigationSplitView(sidebar: {
List(selection: $selectedMeal) {
ForEach(viewModel.filteredMeals) { meal in
MealCardView(meal: meal)
.tag(meal)
}
.listRowSeparator(.hidden, edges: .all)
}
.listStyle(.plain)
.navigationTitle("Find Your Next Meal")
}, detail: {
if let meal = selectedMeal {
DetailView(meal: meal)
} else {
Text("Select a Meal")
}
})
}
How to add a Search Scope
In order to add a search scope, you can simply use the searchScopes modifier where you have to provide a binding to an Identifiable type used for selecting the scope.
I am storing my state property for the selected scope in the view model, where I also will evaluate the filtered meals array:
class MealListViewModel: ObservableObject {
@Published var meals = [Meal]()
@Published var searchText: String = ""
@Published var mealSearchScope: MealSearchScope = .all
...
}
In this case, I show all options for my scope category in a ForEach. The implementation is similar to a SwiftUI picker view.
struct SearchScopeMealListView: View {
@StateObject var viewModel = MealTokenListViewModel()
@State var selectedMeal: Meal? = nil
var body: some View {
NavigationSplitView(sidebar: {
List(selection: $selectedMeal) {
ForEach(viewModel.meals) { meal in
MealCardView(meal: meal)
.tag(meal)
}
.listRowSeparator(.hidden, edges: .all)
}
.listStyle(.plain)
.navigationTitle("Find Your Next Meal")
}, detail: {
if let meal = selectedMeal {
DetailView(meal: meal)
} else {
Text("Select a Meal")
}
})
.searchable(text: $viewModel.searchText)
.searchScopes($viewModel.mealSearchScope, scopes: {
Text("All").tag(MealSearchScope.all)
ForEach(MealCategory.allCases) { category in
Text(category.rawValue)
.tag(MealSearchScope.category(category))
}
})
}
}
On iOS, the search scope area is shown as a picker with a segmented picker style. When the user first enters the search text field, no search scope is displayed. Only once you start typing, will the scope be shown.
The initially selected scope is set to all because I set the initial value for the state property to all
class MealListViewModel: ObservableObject {
...
@Published var mealSearchScope: MealSearchScope = .all
}
On macOS, the search scope is shown in the toolbar above the detail area. Once the user enters the search text field, the search scope area is shown.
Showing Filtered Results
I am currently computing the filtered meal array in the view model with the search term. I can add now the information from the search scope:
class MealListViewModel: ObservableObject {
@Published var meals = [Meal]()
@Published var searchText: String = ""
@Published var mealSearchScope: MealSearchScope = .all
var filteredMeals: [Meal] {
var meals = self.meals
switch mealSearchScope {
case .all: break
case .category(let category):
meals = meals.filter { $0.category == category }
}
if searchText.count > 0 {
meals = meals.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
return meals
}
...
}
Other Filter Options
If you want to support lower iOS and macOS versions, you can show filtering options with SwiftUI Picker. You can see different implementations in this post: “Master SwiftUI Picker View”. For example, you could add a picker or menu view in the toolbar. The picker options would be similar to the tokens.
Summary
Incorporating Search Scopes in your SwiftUI app can significantly improve the search functionality on iOS and macOS. Search Scopes allow users to filter and refine search results by selecting specific categories or filters. This provides users with a more targeted and personalized search experience, enabling them to quickly find the content they need. Additionally, Search Scopes can be especially useful for macOS apps, where users often work with large amounts of data and need to quickly and easily filter results. With Search Scopes, your SwiftUI app can offer a more efficient and intuitive search experience to your users.
Further Reading:
- Use Search with Core Data: SwiftUI and Core Data Course
- How to work with the Search bar in SwiftUI
- Search Tokens for advanced search queries – a must for macOS apps
- Human interface guidelines for search