Un NavigationStack puede incluir una barra de búsqueda, para lo cual se debe aplicar el siguiente modificador:
-
searchable(text:tokens:suggestedTokens:placement:prompt:token:):textes el texto introducido por el usuario.tokenssigue el rastro de los tokens mostrados al usuario.suggestedTokenstiene una lista de los tokens para sugerir valores al usuario.placementes la ubicación de la barra (que puede serautomatic,navigationBarDrawer,navigationBarDrawer(displayMode:),sidebarytoolbar).promptes el placeholder.tokenson las vistas para mostrar los tokens.
struct Person: Identifiable {
let id = UUID()
let name: String
}
private let people: [Person] = [
.init(name: "David Goyes"),
.init(name: "Midoriya Izuku"),
.init(name: "Tanjiro Kamado"),
.init(name: "David Beckham"),
]
struct ContentView: View {
@State private var searchText: String = ""
private var filteredPeople: [Person] {
if searchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(searchText) }
}
}
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text(person.name)
}
.navigationTitle("Personas")
.searchable(text: $searchText, prompt: "¿A quién busca?")
}
}
}
En iOS 26, POR DEFECTO la barra de búsqueda queda en la parte inferior de la pantalla porque tiene SearchFieldPlacement en automatic, que en iOS, MacOS y iPadOS sería lo mismo que toolbar. Cuando la barra se vuelve el "first responder", queda encima del teclado cuando este se presenta.
Notar que para que funcionase el ejemplo anterior, filteredPeople se hizo una variable computada y no almacenada. De lo contrario, habría sido necesario usar una aproximación basada en onChange(of:initial:_:), teniendo en cuenta que se debe marcar initial=true para que se cargue la información en filteredPeople en el arranque.
struct ContentView: View {
@State private var searchText: String = ""
@State private var filteredPeople: [Person] = []
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text(person.name)
}
.navigationTitle("Personas")
.searchable(text: $searchText, prompt: "¿A quién busca?")
.onChange(of: searchText, initial: true) {
filterPeople()
}
}
}
private func filterPeople() {
filteredPeople = if searchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(searchText) }
}
}
}
Visibilidad de la barra de búsqueda
Cuando la barra de búsqueda usa la ubicación por defecto (i.e. automatic) o toolbar, siempre será visible. Si hacemos que aparezca en la barra de navegación con navigationBarDrawer, entonces la barra de búsqueda desaparecerá cuando desplacemos la lista hacia arriba, y volverá a aparecer cuando la desplacemos hacia abajo.
Para que la barra de búsqueda siempre sea visible en la barra de navegación, se puede usar navigationBarDrawer(displayMode:), pasando always como argumento.
struct ContentView: View {
@State private var searchText: String = ""
private var filteredPeople: [Person] {
if searchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(searchText) }
}
}
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text(person.name)
}
.navigationTitle("Personas")
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "¿A quién busca?")
}
}
}
Activando la barra de búsqueda de forma programática
Se puede fijar/quitar el foco sobre una barra de búsqueda de forma programática a través del modificador searchFocused(_:) usando un Binding de tipo Bool con el "property-wrapper" FocusState.
En el siguiente ejemplo, el botón "Buscar" del toolbar, pone isFocused en true, lo que enfoca la barra de búsqueda y muestra el teclado.
struct ContentView: View {
@State private var searchText: String = ""
// ⚠️ Notar el uso de @FocusState
@FocusState private var isFocused: Bool
private var filteredPeople: [Person] {
if searchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(searchText) }
}
}
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text(person.name)
}
.navigationTitle("Personas")
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Button("Buscar", systemImage: "magnifyingglass") {
// ⚠️ Aquí se cambia isFocused para centrar el foco
// en la barra de búsqueda
isFocused = true
}
}
})
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
// ⚠️ por medio de searchFocused se puede cambiar el foco
.searchFocused($isFocused)
}
}
}
Búsqueda deliberadad con onSubmit()
onSubmit(of:_:) permite procesar un valor de un TextField y también decidir cuándo aplicar el filtro de la barra de búsqueda.
En el siguiente código se observa que searchText sea vacío para limpiar el filtro. Esto es útil para cuando el usuario presione el botón de cancelar.
struct ContentView: View {
// ⚠️ searchText solo sirve para almacenar el texto
// del searchbar
@State private var searchText: String = ""
// ⚠️ definiteSearchText será mi filtro final
@State private var definiteSearchText: String = ""
private var filteredPeople: [Person] {
// ⚠️ Notar que ahora se filtra por definiteSearchText
if definiteSearchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(definiteSearchText) }
}
}
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text(person.name)
}
.navigationTitle("Personas")
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
.onChange(of: searchText, {
// ⚠️ Si searchText está vacío, actualizo
// definiteSearchText
if searchText.isEmpty {
definiteSearchText = searchText
}
})
.onSubmit(of: .search) {
// ⚠️ Solo cambio definiteSearchText
// cuando presiono el botón "submit"
// (que está en modo "search")
definiteSearchText = searchText
}
}
}
}
Sugerencias
Se puede presentar una lista de sugerencias al usuario por medio de searchSuggestions(_:). Este modificador recibe un grupo de Text, a los que se debe asociar un término de búsqueda (de tipo String) por medio de searchCompletion(_:).
Cuando se selecciona una sugerencia, en la barra de búsqueda aparece el texto introducido en searchCompletion.
struct ContentView: View {
@State private var searchText: String = ""
@State private var definiteSearchText: String = ""
private var filteredPeople: [Person] {
if definiteSearchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(definiteSearchText) }
}
}
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text(person.name)
}
.navigationTitle("Personas")
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
.onChange(of: searchText, {
if searchText.isEmpty {
definiteSearchText = searchText
}
})
.searchSuggestions({
// ⚠️ No funciona sin searchCompletion
Text("YO").searchCompletion("David")
Text("One for all").searchCompletion("Midoriya")
Text("Medio frío/caliente").searchCompletion("Shoto")
})
.onSubmit(of: .search) {
definiteSearchText = searchText
}
}
}
}
Controlar la búsqueda programáticamente
Las siguientes dos variables de ambiente permiten controlar la barra de búsqueda desde una subvista. NOTA: Solo funciona desde una subvista, porque son inyectadas hacia dentro de la jerarquía. No hacia el contenedor.
-
isSearching: Variable de ambiente que indica que el usuario tiene la barra de búsqueda como "first responder". -
dismissSearch: Variable de ambiente de tipoDismissSearchAction, que puede ser ejecutada como closure, que al llamarse cancela la búsqueda.
struct SearchableContentView: View {
// ⚠️ Referencia a isSearching y dismissSearch desde una vista
// interna a ContentView, que es donde se aplica el .searchable
@Environment(\.isSearching) var isSearching
@Environment(\.dismissSearch) var dismissSearch
let filteredPeople: [Person]
var body: some View {
List {
// ⚠️ Si isSearching = true, se muestra un botón que al
// ser presionado, minimiza la barra de búsqueda.
if isSearching {
Button("Cancelar búsqueda") {
dismissSearch()
}
}
ForEach(filteredPeople) { person in
Text(person.name)
}
}
}
}
struct ContentView: View {
@State private var searchText: String = ""
@State private var definiteSearchText: String = ""
private var filteredPeople: [Person] {
if definiteSearchText.isEmpty {
people
} else {
people.filter { $0.name.localizedStandardContains(definiteSearchText) }
}
}
var body: some View {
NavigationStack {
// ⚠️ Notar que contiene SearchableContentView
SearchableContentView(filteredPeople: filteredPeople)
.navigationTitle("Personas")
// ⚠️ Se aplica .searchable en el contenedor sobre la vista
// contenida
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
.onChange(of: searchText, {
if searchText.isEmpty {
definiteSearchText = searchText
}
})
.onSubmit(of: .search) {
definiteSearchText = searchText
}
}
}
}
Mismo texto de búsqueda, distintos lugares para buscar
Puede ser que tengamos una lista de estructuras de datos que tienen varias variables sobre las que podríamos aplicar el mismo texto de búsqueda. En caso de que se desee permitir al usuario seleccionar una de estas variables, se debe definir un "scope" de búsqueda por medio de searchScopes(_:scopes:) o searchScopes(_:activation:_:). Este modificador recibe, similar a las sugerencias, recibe un closure con una colección de objetos tipo Text a los cuales se va a asignar una etiqueta con el modificador tag(_:includeOptional:).
El modificador searchScopes(_:scopes:) muestra el Picker con los "scopes" de búsqueda tan pronto se escribe la primera letra en la barra de búsqueda.
También es posible hacer que el Picker sea visible tan pronto la barra de búsqueda obtiene el foco, si se usa el modificador searchScopes(_:activation:_:) junto con el criterio de activación SearchScopeActivation.onSearchPresentation.
// ⚠️ Se definen los scopes de búsqueda
enum SearchScope {
case name, lastname
}
struct ContentView: View {
@State private var searchText: String = ""
@State private var definiteSearchText: String = ""
// ⚠️ Este es el "scope" seleccionado
@State private var scope: SearchScope = .name
private var filteredPeople: [Person] {
if definiteSearchText.isEmpty {
people
} else {
people.filter {
// ⚠️ Se usa el nombre o el apellido para buscar
// según el scope seleccionado
let value = scope == .name ? $0.name : $0.lastname
return value.localizedStandardContains(definiteSearchText) }
}
}
var body: some View {
NavigationStack {
List(filteredPeople) { person in
Text("\(person.name) \(person.lastname)")
}
.navigationTitle("Personas")
// ⚠️ Se instala la barra de búsqueda
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "¿A quién busca?")
// ⚠️ Se definen unos "scopes" para pintar un Picker
.searchScopes($scope, activation: .onTextEntry) {
// ⚠️ Los "scopes" tienen tags.
Text("Nombre").tag(SearchScope.name)
Text("Apellido").tag(SearchScope.lastname)
}
.onChange(of: searchText, {
if searchText.isEmpty {
definiteSearchText = searchText
}
})
.onSubmit(of: .search) {
definiteSearchText = searchText
}
}
}
}










Top comments (0)