Updated for Xcode 14.2
New in iOS 16
SwiftUI’s searchable()
modifier lets us place a search bar directly into a NavigationStack
, but along with just free-text search we can also allow the user to select search tokens – pre-filled chunks of text that represent a specific category or filter in your app.
This isn’t hard to do, but it does require several steps. You need:
searchable()
implementation that filters your results by the user’s search text.Identifiable
.Text
view, but it doesn’t need to be.That might not sound too complex, but there’s an extra wrinkle: the iOS implementation of searchable()
will replace your search results with your suggested tokens by default, which makes the default search functionality a lot less useful. So, I prefer to ask users to activate token filtering specifically by starting with a “#” sign, similar to Twitter and Mastodon.
Anyway, enough talk – here’s a sample implementation of searchable()
with token support:
// Holds one uniquely identifiable movie.
struct Movie: Identifiable {
var id = UUID()
var name: String
var genre: String
}
// Holds one token that we want the user to filter by. This *must* conform to Identifiable.
struct Token: Identifiable {
var id: String { name }
var name: String
}
struct ContentView: View {
// Whatever text the user has typed so far.
@State private var searchText = ""
// All possible tokens we want to show to the user.
let allTokens = [Token(name: "Action"), Token(name: "Comedy"), Token(name: "Drama"), Token(name: "Family"), Token(name: "Sci-Fi")]
// The list of tokens the user currently has selected.
@State private var currentTokens = [Token]()
// The list of tokens we want to show to the user right now. Activates token selection only when searchText starts with #.
var suggestedTokens: [Token] {
if searchText.starts(with: "#") {
return allTokens
} else {
return []
}
}
// Some data to show and filter by.
let movies = [
Movie(name: "Avatar", genre: "Sci-Fi"),
Movie(name: "Inception", genre: "Sci-Fi"),
Movie(name: "Love Actually", genre: "Comedy"),
Movie(name: "Paddington", genre: "Family")
]
// The real work: filter all the movies based on search text or tokens.
var searchResults: [Movie] {
// trim whitespace
let trimmedSearchText = searchText.trimmingCharacters(in: .whitespaces)
return movies.filter { movie in
if searchText.isEmpty == false {
// If we have search text, make sure this item matches.
if movie.name.localizedCaseInsensitiveContains(trimmedSearchText) == false {
return false
}
}
if currentTokens.isEmpty == false {
// If we have search tokens, loop through them all to make sure one of them matches our movie.
for token in currentTokens {
if token.name.localizedCaseInsensitiveContains(movie.genre) {
return true
}
}
// This movie does *not* match any of our tokens, so it shouldn't be sent back.
return false
}
// If we're still here then the movie should be included.
return true
}
}
var body: some View {
NavigationStack {
List(searchResults) { movie in
Text(movie.name)
}
.navigationTitle("Movies+")
.searchable(text: $searchText, tokens: $currentTokens, suggestedTokens: .constant(suggestedTokens), prompt: Text("Type to filter, or use # for tags")) { token in
Text(token.name)
}
}
}
}
Download this as an Xcode project
There are a few things that are worth pointing out in that code:
.constant(suggestedTokens)
.searchable()
prompt explicitly tells the user to type a “#” for tags.searchable()
lets us tell SwiftUI to render each tag as some text showing its name.In practice, I suspect you’re more likely to have multiple tags attached to each piece of data you’re working with, in which case I’d probably prefer Swift’s isSuperset(of:)
set operation for comparing the user’s selected tags against those in your object. If you’re working with lots of tokens, I would also suggest you filter your list of suggested tokens based on what the user has typed so far.
One last thing: although the iOS implementation of searchable()
replaces your search results with the suggested tokens, this does not happen on macOS. Instead, your search tokens appear as a popup below the search box, leaving your search results visible at the same time – it’s a much nicer experience.
SPONSORED From March 20th to 26th, you can join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.