DEV Community

GoyesDev
GoyesDev

Posted on • Edited on

[SUI] Barra de búsqueda

Un NavigationStack puede incluir una barra de búsqueda, para lo cual se debe aplicar el siguiente modificador:

struct Person: Identifiable {
  let id = UUID()
  let name: String
}
Enter fullscreen mode Exit fullscreen mode
private let people: [Person] = [
  .init(name: "David Goyes"),
  .init(name: "Midoriya Izuku"),
  .init(name: "Tanjiro Kamado"),
  .init(name: "David Beckham"),
]
Enter fullscreen mode Exit fullscreen mode
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?")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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) }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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?")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 tipo DismissSearchAction, 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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode


Bibliografía

Top comments (0)