Programación en Swift y SwiftUI para iOS Developers

Cómo ocultar el teclado en SwiftUI

Introducción: El Invitado que No Sabe Cuándo Irse

En el desarrollo de aplicaciones móviles, pocas cosas rompen tanto la inmersión y la experiencia de usuario (UX) como un teclado que se niega a desaparecer. Has diseñado una interfaz limpia, minimalista y funcional en Xcode. El usuario toca un campo de texto, el teclado virtual se desliza suavemente hacia arriba… y ahí se queda.

El usuario termina de escribir, toca el botón de “Enviar”, o quizás simplemente quiere hacer scroll para ver el contenido que ha quedado oculto bajo las teclas. Toca el fondo, desliza el dedo, pero el teclado permanece estoico, cubriendo el 40% de la pantalla.

En el antiguo paradigma de UIKit, los desarrolladores veteranos recordarán la danza ritual de anular touchesBegan o gestionar el First Responder manualmente en cada UIViewController. Pero, ¿cómo traducimos esto al mundo declarativo de SwiftUI? ¿Es más fácil? ¿Es más difícil?

La respuesta corta es: Es diferente. Y afortunadamente, con las últimas versiones de iOS, es mucho más potente.

En este tutorial masivo, vamos a diseccionar cada método existente para descartar u ocultar el teclado en SwiftUI. No nos limitaremos a copiar y pegar un fragmento de código; entenderemos la arquitectura subyacente, exploraremos las diferencias entre iOS 14, 15 y 16+, y crearemos soluciones reutilizables de nivel profesional.


Parte 1: Entendiendo la Teoría del “First Responder”

Antes de escribir una sola línea de código en Swift, es vital entender qué está ocurriendo bajo el capó. Aunque SwiftUI abstrae mucha complejidad, la gestión del teclado sigue basándose en el concepto de Responder Chain (Cadena de Responsabilidad) de UIKit.

Cuando un usuario toca un TextField o un TextEditor, esa vista solicita convertirse en el First Responder (Primer Respondedor). El sistema operativo le concede este estado y, como consecuencia, despliega el teclado para permitir la entrada de datos.

Para “ocultar” el teclado, no le decimos al teclado que se cierre. Técnicamente, le decimos a la vista activa: “Por favor, renuncia a tu estado de First Responder”. Al hacerlo, el sistema detecta que ya no hay ninguna vista solicitando entrada de texto y retira el teclado automáticamente.

En SwiftUI, tenemos tres estrategias principales para lograr esto:

  1. La Estrategia Global (Legacy): Forzar la renuncia globalmente.
  2. La Estrategia de Estado (Modern): Usar @FocusState.
  3. La Estrategia de Comportamiento (Nativa): Usar modificadores de Scroll.

Exploremos cada una en detalle.


Parte 2: La Solución Universal (El método UIApplication)

Esta es la técnica más conocida y utilizada en los primeros días de SwiftUI. Sigue siendo útil porque funciona independientemente de la jerarquía de vistas y es compatible con versiones antiguas de iOS (13/14).

La idea es enviar una acción a la aplicación entera diciéndole: “Quien quiera que tenga el foco ahora mismo, que lo suelte”.

El Código Base

Primero, extendemos UIApplication para facilitar la llamada:

import SwiftUI

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

Análisis técnico:

  • sendAction: Es un método que envía un mensaje de control a través de la cadena de respondedores.
  • to: nil: Este es el truco. Al enviar la acción a nil, el sistema busca automáticamente al primer objeto que sea capaz de responder a la acción resignFirstResponder. Ese objeto es, invariablemente, el campo de texto activo.

Implementación en la Vista

Ahora, aplicamos un gesto de toque (TapGesture) en nuestra vista contenedora.

struct OldSchoolFormView: View {
    @State private var text: String = ""

    var body: some View {
        ZStack {
            Color.gray.opacity(0.1).ignoresSafeArea() // Fondo visual
            
            VStack {
                TextField("Escribe algo aquí...", text: $text)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                Button("Botón de Acción") {
                    print("Acción realizada")
                }
            }
        }
        // Detectamos el toque en cualquier parte del ZStack
        .onTapGesture {
            UIApplication.shared.endEditing()
        }
    }
}

El problema del “Espacio Vacío” (Muy Importante) Un error común de los principiantes es aplicar .onTapGesture a un VStack que tiene mucho espacio en blanco. En SwiftUI, los contenedores como VStack o HStack no tienen “cuerpo” en los espacios vacíos; son transparentes a los toques. Si tocas entre dos elementos, el toque atraviesa la vista y el gesto no se activa.

La Solución: Usa .contentShape(Rectangle()).

VStack {
    // Tus campos
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle()) // Hace que toda el área sea "tocable"
.onTapGesture {
    UIApplication.shared.endEditing()
}

Parte 3: El Estándar Moderno – @FocusState (iOS 15+)

Con la llegada de iOS 15, Apple nos dio una herramienta nativa y puramente “Swifty” para manejar esto: el Property Wrapper @FocusState.

Esta aproximación es superior porque es declarativa. En lugar de enviar una orden imperativa (“¡Cierra el teclado!”), simplemente cambiamos una variable de estado (“El foco ahora es falso”) y la UI reacciona en consecuencia.

Caso 1: Un solo campo

struct ModernLoginView: View {
    @State private var email: String = ""
    
    // 1. Declaramos el estado del foco
    @FocusState private var isEmailFocused: Bool

    var body: some View {
        VStack {
            TextField("Correo electrónico", text: $email)
                // 2. Vinculamos el campo al estado
                .focused($isEmailFocused)
                .textFieldStyle(.roundedBorder)
                .padding()
            
            Button("Cerrar Teclado") {
                // 3. Modificamos el estado para cerrar
                isEmailFocused = false
            }
        }
        .onTapGesture {
            isEmailFocused = false
        }
    }
}

Cuando isEmailFocused cambia a true, el teclado sube. Cuando cambia a false, baja. Es bidireccional y reactivo.

Caso 2: Gestión Avanzada de Formularios (Múltiples Campos)

Imagina un formulario de registro con Nombre, Apellido y Dirección. Crear un booleano para cada uno (isNameFocusedisLastNameFocused…) es sucio y difícil de mantener.

@FocusState brilla aquí porque puede trabajar con tipos Hashable, como un Enum. Esto nos permite crear una máquina de estados para nuestro foco.

struct RegistrationFormView: View {
    // Definimos los campos posibles
    enum Field: Hashable {
        case name
        case surname
        case email
        case password
    }

    @State private var name = ""
    @State private var surname = ""
    @State private var email = ""
    @State private var password = ""
    
    // El estado ahora es de tipo Field opcional
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            Section(header: Text("Datos Personales")) {
                TextField("Nombre", text: $name)
                    .focused($focusedField, equals: .name)
                    .submitLabel(.next) // Cambia la tecla "Return" a "Siguiente"
                
                TextField("Apellido", text: $surname)
                    .focused($focusedField, equals: .surname)
                    .submitLabel(.next)
            }
            
            Section(header: Text("Cuenta")) {
                TextField("Email", text: $email)
                    .focused($focusedField, equals: .email)
                    .keyboardType(.emailAddress)
                    .submitLabel(.next)
                
                SecureField("Contraseña", text: $password)
                    .focused($focusedField, equals: .password)
                    .submitLabel(.done)
            }
        }
        // Lógica para saltar entre campos automáticamente
        .onSubmit {
            switch focusedField {
            case .name:
                focusedField = .surname
            case .surname:
                focusedField = .email
            case .email:
                focusedField = .password
            case .password:
                focusedField = nil // Cierra el teclado al final
            default:
                break
            }
        }
        // Tocar fuera cierra todo
        .onTapGesture {
            focusedField = nil
        }
    }
}

¿Por qué es esto brillante?

  1. UX Superior: El usuario puede navegar por el formulario usando la tecla “Siguiente” del teclado, sin tener que tocar la pantalla.
  2. Código Limpio: Toda la lógica de navegación está centralizada en el bloque .onSubmit.
  3. Control Total: Tienes control programático exacto sobre qué campo está activo en cada momento.

Parte 4: La Solución Nativa de Scroll (iOS 16+)

A partir de iOS 16, SwiftUI introdujo un modificador que imita el comportamiento nativo que vemos en aplicaciones como Mensajes o WhatsApp: el teclado se oculta cuando el usuario arrastra la lista hacia abajo.

El modificador es: .scrollDismissesKeyboard(_ mode:)

struct ChatView: View {
    @State private var message = ""
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<50) { i in
                    Text("Mensaje \(i)")
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding()
                        .background(Color.blue.opacity(0.1))
                        .cornerRadius(8)
                }
            }
            .padding()
        }
        // MAGIA AQUÍ:
        .scrollDismissesKeyboard(.interactively)
        
        // Input bar
        HStack {
            TextField("Escribir mensaje...", text: $message)
                .textFieldStyle(.roundedBorder)
        }
        .padding()
    }
}

Los modos disponibles son:

  • .interactively: El teclado baja siguiendo el dedo del usuario (comportamiento estándar de iOS).
  • .immediately: El teclado desaparece en cuanto comienza el scroll.
  • .never: Mantiene el teclado visible (útil para editores de notas).

Limitación: Este método solo funciona si el usuario hace scroll. Si la lista es corta y no hay scroll, el teclado no se ocultará. Por eso, se recomienda combinar este método con el onTapGesture explicado anteriormente para una cobertura total.


Parte 5: El Problema del Teclado Numérico

Hay un “villano” en esta historia: El NumberPad.

A diferencia del teclado estándar, el teclado numérico en iPhone no tiene tecla de retorno. Esto significa que, si no implementas una forma de cerrarlo, el usuario puede quedarse atrapado para siempre en ese campo.

La solución elegante es añadir una Toolbar (Barra de herramientas) sobre el teclado.

struct AmountInputView: View {
    @State private var amount = ""
    @FocusState private var isInputActive: Bool

    var body: some View {
        TextField("Cantidad", text: $amount)
            .keyboardType(.decimalPad)
            .focused($isInputActive)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    // Botón espaciador para empujar el "Hecho" a la derecha
                    Spacer()
                    
                    Button("Hecho") {
                        isInputActive = false
                    }
                    .fontWeight(.bold)
                }
            }
    }
}

Este código añade una barra nativa encima de los números con un botón “Hecho” a la derecha, proporcionando una salida clara y accesible para el usuario.


Parte 6: Ingeniería de Software – Creando un ViewModifier Reutilizable

Como buenos ingenieros, debemos cumplir el principio DRY (Don’t Repeat Yourself). Escribir .onTapGesture { ... } en cada vista es propenso a errores y sucio.

Vamos a encapsular la lógica en un modificador personalizado que podamos reutilizar en cualquier proyecto.

import SwiftUI

// 1. Definimos la estructura del modificador
struct DismissKeyboardOnTap: ViewModifier {
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                UIApplication.shared.sendAction(
                    #selector(UIResponder.resignFirstResponder),
                    to: nil,
                    from: nil,
                    for: nil
                )
            }
    }
}

// 2. Extendemos View para una sintaxis más limpia
extension View {
    func dismissKeyboardOnTap() -> some View {
        modifier(DismissKeyboardOnTap())
    }
}

Uso final en tu código:

var body: some View {
    VStack {
        // Tu formulario complejo
    }
    .dismissKeyboardOnTap() // ¡Una sola línea limpia!
}

Consideraciones de UX y Accesibilidad

Descartar el teclado es más que un requisito técnico; es una cuestión de usabilidad. Aquí hay algunos consejos para elevar la calidad de tu app:

  1. No seas agresivo: Evita poner el onTapGesture en elementos muy pequeños o cerca de botones. Puede frustrar al usuario si cierra el teclado accidentalmente cuando intentaba pulsar otra cosa.
  2. iPadOS y Hardware: Recuerda que los usuarios de iPad a menudo usan teclados físicos. Ellos esperan que la combinación Cmd + . o la tecla Esc (en algunos contextos) cierre la edición. @FocusState maneja esto bastante bien, pero asegúrate de probar tu app con un teclado Bluetooth conectado.
  3. Animaciones: Cuando usas @FocusState, el cierre del teclado se anima con el sistema. Si intentas manipular la posición de la vista manualmente mientras el teclado baja, asegúrate de envolver tus cambios de estado en withAnimation.

Resumen y Guía de Decisión

¿Te sientes abrumado con las opciones? Aquí tienes tu hoja de ruta para decidir qué usar:

  1. ¿Estás haciendo un Chat o una Lista larga?
    • Usa .scrollDismissesKeyboard(.interactively).
  2. ¿Tienes un formulario complejo con validaciones?
    • Usa @FocusState con un Enum para gestionar la navegación entre campos.
  3. ¿Tienes un campo numérico?
    • Obligatoriamente añade una .toolbar con botón de cierre.
  4. ¿Necesitas una solución rápida para una pantalla sencilla?
    • Usa el ViewModifier personalizado con .onTapGesture en el contenedor principal.

Conclusión

SwiftUI ha madurado increíblemente en los últimos años. Lo que antes requería hacks de Objective-C y UIKit, ahora se puede lograr con APIs declarativas elegantes y seguras.

Dominar el teclado es el primer paso para dominar la calidad de una aplicación. Un teclado bien gestionado es invisible para el usuario; simplemente “funciona” como se espera. Un teclado mal gestionado es motivo de desinstalación.

Ahora tienes en tu arsenal todas las herramientas posibles para asegurarte de que tu app caiga en la primera categoría. ¡Abre Xcode y empieza a limpiar esa interfaz!

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

Cómo ocultar el teclado al pulsar desde afuera de la pantalla en SwiftUI

Next Article

Cómo mostrar un Bottom Sheet en SwiftUI

Related Posts