Programación en Swift y SwiftUI para iOS Developers

Cómo añadir una barra de búsqueda en SwiftUI

En la era moderna del desarrollo de aplicaciones, la capacidad de encontrar contenido rápidamente no es una característica opcional; es una necesidad. Para un iOS Developer, la implementación de barras de búsqueda solía implicar delegados complejos, controladores de búsqueda y una gestión tediosa del estado de la UI en UIKit.

Sin embargo, con la madurez de la programación Swift y la evolución de SwiftUI, Apple nos ha regalado uno de los modificadores más potentes y elegantes del framework: .searchable().

En este tutorial exhaustivo, aprenderemos a añadir una barra de busqueda en SwiftUI, gestionaremos el filtrado de datos en tiempo real y adaptaremos nuestra implementación para que brille tanto en iOS, como en macOS y watchOS, todo desde Xcode.

¿Qué es el modificador .searchable()?

Introducido en iOS 15, .searchable() es un modificador de vista que marca un contenedor (normalmente un NavigationStack o NavigationSplitView) como “buscable”.

A diferencia de los enfoques imperativos antiguos, donde tú tenías que dibujar la barra de búsqueda, SwiftUI se encarga de renderizar la barra de búsqueda nativa del sistema en la ubicación correcta según la plataforma. Esto garantiza que tu aplicación siempre se sienta nativa, ya sea en un iPhone, un iPad, un Mac o un Apple Watch.

¿Por qué es vital para el iOS Developer moderno?

  1. Declarativo: Defines qué quieres (una barra de búsqueda), no cómo dibujarla.
  2. Contextual: Se adapta automáticamente. En iOS aparece bajo la barra de navegación; en macOS, en la barra de herramientas.
  3. Integrado: Gestiona las animaciones de presentación, el foco del teclado y el botón de “cancelar” automáticamente.

Implementación Básica en iOS

Vamos a empezar con el escenario más común: una lista de contactos en un iPhone. Para esto, necesitamos una fuente de verdad (nuestros datos) y una variable de estado para almacenar lo que el usuario escribe.

Paso 1: Configuración del Estado

Abre Xcode y crea una nueva vista. Necesitaremos una propiedad @State para enlazar el texto de la búsqueda.

import SwiftUI

struct ContactsView: View {
    // 1. El texto que el usuario escribe
    @State private var searchText = ""
    
    // Datos de ejemplo
    let names = ["Alejandro", "Berta", "Carlos", "Diana", "Elena", "Fernando", "Gabriela"]
    
    var body: some View {
        NavigationStack {
            List {
                // Paso 2: Mostramos los datos (filtrados más adelante)
                ForEach(names, id: \.self) { name in
                    Text(name)
                }
            }
            .navigationTitle("Contactos")
            // Paso 3: La magia de SwiftUI
            .searchable(text: $searchText, prompt: "Buscar contacto")
        }
    }
}

Al añadir .searchable(text: $searchText), SwiftUI inyecta automáticamente la barra de búsqueda. El parámetro prompt es el texto placeholder que guía al usuario.

Paso 2: La Lógica de Filtrado

El modificador .searchable() no filtra los datos por ti; simplemente te da el texto que el usuario ha escrito. Como buen iOS Developer, debes implementar la lógica.

La forma más limpia de hacer esto en programación Swift es mediante una propiedad computada:

var filteredNames: [String] {
    if searchText.isEmpty {
        return names
    } else {
        return names.filter { $0.localizedCaseInsensitiveContains(searchText) }
    }
}

Ahora, actualizamos nuestro ForEach para usar filteredNames en lugar de names.

List {
    ForEach(filteredNames, id: \.self) { name in
        Text(name)
    }
}

Con este simple cambio, tienes una búsqueda funcional en tiempo real. localizedCaseInsensitiveContains es crucial aquí, ya que ignora mayúsculas y minúsculas y respeta las reglas del idioma del dispositivo.

Elevando el Nivel: Sugerencias de Búsqueda (Search Suggestions)

Una excelente experiencia de búsqueda no solo espera a que el usuario termine de escribir; le guía. SwiftUI permite añadir sugerencias visuales que aparecen antes o durante la escritura. Esto es especialmente útil en aplicaciones de comercio electrónico o contenido multimedia.

.searchable(text: $searchText) {
    // Estas vistas aparecen cuando el usuario toca la barra de búsqueda
    // pero aún no ha escrito mucho o nada.
    if searchText.isEmpty {
        Text("Recientes").font(.caption).foregroundStyle(.secondary)
        Button("Carlos") { searchText = "Carlos" }
        Button("Elena") { searchText = "Elena" }
    } else {
        // Sugerencias basadas en lo que escribe
        ForEach(names.filter { $0.hasPrefix(searchText) }, id: \.self) { name in
            Text("¿Buscas \(name)?").searchCompletion(name)
        }
    }
}

El modificador .searchCompletion(_:) es una joya. Cuando el usuario toca esa fila, SwiftUI rellena automáticamente la barra de búsqueda con ese texto y ejecuta la búsqueda.

Adaptación Multiplataforma: macOS y watchOS

Uno de los pilares de Swift y SwiftUI es la capacidad de escribir una vez y desplegar en todas partes. Sin embargo, añadir una barra de busqueda en swiftui tiene matices según el dispositivo.

El Comportamiento en macOS

En macOS, las convenciones de diseño son distintas. Una barra de búsqueda no suele desplazarse con el contenido; suele vivir fija en la parte superior derecha de la ventana o en la barra lateral. Al usar .searchable en macOS dentro de un NavigationSplitView, Xcode compilará una interfaz nativa de Mac.

struct MacContentView: View {
    @State private var searchText = ""
    
    var body: some View {
        NavigationSplitView {
            List(filteredNames, id: \.self) { name in
                NavigationLink(name, destination: Text("Detalles de \(name)"))
            }
            .searchable(text: $searchText, placement: .sidebar) // Específico para Mac
        } detail: {
            Text("Selecciona un contacto")
        }
    }
}

El parámetro placement es vital aquí.

  • .sidebar: Coloca la búsqueda en la parte superior de la lista lateral (estándar en Finder, Mail).
  • .toolbar: La coloca en la barra de herramientas superior derecha.

El Desafío de watchOS

En el Apple Watch, la pantalla es pequeña. Al usar .searchable, el sistema colapsa la barra de búsqueda en un botón icónico (lupa) en la parte superior de la lista. Al pulsarlo, entra en un modo de entrada de texto (dictado o teclado QWERTY en Series 7+).

struct WatchContactsView: View {
    @State private var searchText = ""
    
    var body: some View {
        NavigationStack {
            List {
                // Contenido...
            }
        }
        .searchable(text: $searchText)
    }
}

Nota importante: En watchOS, asegúrate de que .searchable esté aplicado al contenedor de navegación superior, no a elementos internos, para asegurar que el sistema lo dibuje correctamente bajo el título de navegación.

Técnicas Avanzadas para el iOS Developer Senior

Para diferenciarte como experto en programación Swift, debes manejar los casos de borde y el rendimiento.

1. Scopes (Ámbitos de Búsqueda)

A veces, buscar por nombre no es suficiente. ¿Qué pasa si quieres buscar por “Nombre” o por “Teléfono”? Aquí entran los Search Scopes.

enum SearchScope: String, CaseIterable {
    case name = "Nombre"
    case phone = "Teléfono"
}

struct ScopedSearchView: View {
    @State private var searchText = ""
    @State private var searchScope: SearchScope = .name
    
    var body: some View {
        NavigationStack {
            List { ... }
            .searchable(text: $searchText)
            .searchScopes($searchScope) {
                ForEach(SearchScope.allCases, id: \.self) { scope in
                    Text(scope.rawValue).tag(scope)
                }
            }
        }
    }
}

Esto añade automáticamente una barra de segmentos debajo del campo de búsqueda en iOS, permitiendo al usuario refinar su intención al instante.

2. Tokens (iOS 16+)

Similar a la app de Mail o Finder, los Tokens permiten búsquedas complejas combinando filtros.

// Requiere definir un modelo que conforme a Identifiable
.searchable(text: $text, tokens: $selectedTokens, suggestedTokens: $suggestions) { token in
    Text(token.description)
}

3. Optimización de Rendimiento (Debouncing)

Cuando buscas en una base de datos local (CoreData/SwiftData), la búsqueda en tiempo real es rápida. Pero si tu búsqueda realiza una llamada a una API REST, no puedes permitirte hacer una petición HTTP por cada letra que el usuario escribe (“H”, “Ho”, “Hol”, “Hola”).

Debemos implementar “Debouncing” (rebote). Aunque podemos usar Combine, Swift moderno con Concurrency nos permite hacerlo de forma más limpia usando .task.

struct RemoteSearchView: View {
    @State private var searchText = ""
    @State private var results: [String] = []
    
    var body: some View {
        NavigationStack {
            List(results, id: \.self) { item in
                Text(item)
            }
            .searchable(text: $searchText)
            .onChange(of: searchText) { oldValue, newValue in
                // Cancelamos tareas anteriores automáticamente al cambiar el valor
                Task {
                    // Esperamos 500ms antes de disparar la búsqueda
                    try? await Task.sleep(nanoseconds: 500 * 1_000_000)
                    
                    // Si el usuario siguió escribiendo, esta tarea se cancela 
                    // y la siguiente línea nunca se ejecuta.
                    if !Task.isCancelled {
                        await performRemoteSearch(query: newValue)
                    }
                }
            }
        }
    }
    
    func performRemoteSearch(query: String) async {
        // Tu llamada a la API aquí
    }
}

Este patrón evita saturar tu servidor y ahorra batería y datos al usuario.

4. Gestionando el Estado con Environment

A veces necesitas saber programáticamente si el usuario está buscando para ocultar elementos de la UI (como un botón flotante o un carrusel de imágenes). SwiftUI expone esta variable de entorno:

@Environment(\.isSearching) private var isSearching

var body: some View {
    VStack {
        if !isSearching {
            HeroImageBanner() // Se oculta al empezar a buscar
        }
        List { ... }
    }
    .searchable(text: $text)
}

Mejorando la UX: ContentUnavailableView

Con iOS 17 y Xcode 15, Apple introdujo una forma estándar de mostrar estados vacíos. Cuando un usuario busca algo que no existe, mostrar una pantalla en blanco es una mala experiencia.

List {
    ForEach(filteredNames, id: \.self) { name in
        Text(name)
    }
}
.overlay {
    if filteredNames.isEmpty && !searchText.isEmpty {
        ContentUnavailableView.search(text: searchText)
    }
}

ContentUnavailableView.search genera automáticamente un icono de lupa con el texto “No se encontraron resultados para ‘xyz'”, manteniendo la consistencia del sistema operativo.

Conclusión

Añadir una barra de busqueda en SwiftUI ha pasado de ser una tarea tediosa en UIKit a una experiencia declarativa y potente. El modificador .searchable() encapsula años de mejores prácticas de interfaz de usuario de Apple en una sola línea de código.

Como iOS Developer, tu responsabilidad ahora se centra en la lógica de los datos y en ofrecer una experiencia fluida (usando debouncing y scopes), dejando que SwiftUI se encargue de dibujar los píxeles perfectos en iOS, macOS y watchOS. Dominar estas herramientas de programación Swift no solo hace que tus apps se vean mejor, sino que las hace más accesibles y fáciles de usar, factores críticos para el éxito en el App Store.

Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Mejores patrones de diseño en SwiftUI

Next Article

Cómo seleccionar múltiples fechas en SwiftUI

Related Posts