Add fourteen skills from Dimillian/Skills, integrate the merged Snowflake and WordPress updates into the maintainer sync, and refresh registry metadata, attributions, walkthrough notes, and the 8.9.0 release notes while keeping validation warnings within budget.
1.7 KiB
1.7 KiB
Searchable
Intent
Use searchable to add native search UI with optional scopes and async results.
Core patterns
- Bind
searchable(text:)to local state. - Use
.searchScopesfor multiple search modes. - Use
.task(id: searchQuery)or debounced tasks to avoid overfetching. - Show placeholders or progress states while results load.
Example: searchable with scopes
@MainActor
struct ExploreView: View {
@State private var searchQuery = ""
@State private var searchScope: SearchScope = .all
@State private var isSearching = false
@State private var results: [SearchResult] = []
var body: some View {
List {
if isSearching {
ProgressView()
} else {
ForEach(results) { result in
SearchRow(result: result)
}
}
}
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("Search")
)
.searchScopes($searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.title)
}
}
.task(id: searchQuery) {
await runSearch()
}
}
private func runSearch() async {
guard !searchQuery.isEmpty else {
results = []
return
}
isSearching = true
defer { isSearching = false }
try? await Task.sleep(for: .milliseconds(250))
results = await fetchResults(query: searchQuery, scope: searchScope)
}
}
Design choices to keep
- Show a placeholder when search is empty or has no results.
- Debounce input to avoid spamming the network.
- Keep search state local to the view.
Pitfalls
- Avoid running searches for empty strings.
- Don’t block the main thread during fetch.