Programación en Swift y SwiftUI para iOS Developers

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

Introducción: El Problema de la UX “Pegajosa”

Si llevas algún tiempo desarrollando aplicaciones en iOS, seguramente te has encontrado con este escenario frustrante: Diseñas una interfaz hermosa, colocas un campo de texto (TextField o TextEditor), ejecutas la aplicación en el simulador, escribes algo y… el teclado se queda ahí. Ocupando la mitad de la pantalla. Tapas el fondo, tocas otros elementos, intentas deslizar, pero el teclado sigue visible, ocultando botones importantes o información vital.

A diferencia de otros frameworks o comportamientos nativos en Android, iOS no siempre descarta el teclado automáticamente cuando el usuario toca fuera de un campo de entrada. En el antiguo mundo de UIKit, solíamos solucionar esto anulando el método touchesBegan en el controlador de vista principal. Pero, ¿cómo se logra esto en el mundo declarativo de SwiftUI?

En este tutorial masivo, exploraremos todas las formas de gestionar el cierre del teclado en SwiftUI. Desde soluciones rápidas para prototipos hasta implementaciones robustas para aplicaciones de producción a gran escala, cubriendo las diferencias entre iOS 15, iOS 16 y posteriores.


Entendiendo el Concepto: “First Responder”

Antes de escribir código, es crucial entender qué está pasando bajo el capó. En el ecosistema de Apple, cuando un usuario toca un campo de texto, ese campo se convierte en el First Responder (Primer Respondedor). Esto significa que es el objeto activo esperando recibir eventos (en este caso, pulsaciones de teclas).

Para ocultar el teclado, técnicamente no le decimos al teclado que se “esconda”; le decimos al campo de texto que renuncie a su estado de First Responder (resignFirstResponder).

En SwiftUI, la gestión del estado es diferente, pero el principio subyacente sigue interactuando con este concepto de UIKit.


Método 1: La Solución Global (Rápida y Sucia)

La forma más común y compatible con versiones anteriores de SwiftUI para lograr esto es detectar un toque (TapGesture) en la vista contenedora y forzar el cierre de la edición en toda la ventana de la aplicación.

El Código

Podemos extender View o UIApplication para crear una función reutilizable.

import SwiftUI

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

Cómo implementarlo en tu Vista

Una vez que tienes esa extensión, puedes aplicarla a la vista principal de tu jerarquía (como un ZStack o VStack que contenga todo el contenido).

struct LoginView: View {
    @State private var email: String = ""
    @State private var password: String = ""

    var body: some View {
        ZStack {
            Color.white.ignoresSafeArea() // Fondo
            
            VStack(spacing: 20) {
                TextField("Correo electrónico", text: $email)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                
                SecureField("Contraseña", text: $password)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                
                Button("Iniciar Sesión") {
                    // Lógica de login
                }
            }
            .padding()
        }
        // Aquí ocurre la magia
        .onTapGesture {
            UIApplication.shared.endEditing()
        }
    }
}

Análisis Técnico

  • Ventajas: Es fácil de entender y funciona en casi todas las versiones de SwiftUI.
  • Desventajas: onTapGesture puede ser agresivo. Si tienes botones u otros elementos interactivos dentro de esa vista, a veces el gesto de toque en el fondo puede entrar en conflicto con gestos en elementos hijos si no se configuran correctamente. Además, depende de UIApplication, lo cual rompe un poco la pureza del paradigma declarativo de SwiftUI.

Método 2: El Enfoque Moderno con @FocusState (iOS 15+)

Con la llegada de iOS 15, Apple nos dio una herramienta nativa y puramente SwiftUI para manejar el foco: el Property Wrapper @FocusState. Esta es la forma “correcta” y canónica de hacerlo hoy en día.

¿Cómo funciona?

@FocusState funciona de manera similar a @State, pero está diseñado específicamente para rastrear qué campo tiene el foco. Es una variable booleana (o un enum para múltiples campos) que es verdadera cuando el teclado está arriba y falsa cuando no.

Implementación Paso a Paso

  1. Declaramos la variable de estado de foco.
  2. Vinculamos el TextField a esa variable.
  3. Cambiamos la variable a false cuando tocamos fuera.
struct RegistrationView: View {
    @State private var name: String = ""
    
    // 1. Declarar el estado del foco
    @FocusState private var isInputActive: Bool

    var body: some View {
        VStack {
            TextField("Tu nombre completo", text: $name)
                // 2. Vincular el foco
                .focused($isInputActive)
                .textFieldStyle(.roundedBorder)
                .padding()
            
            Spacer()
        }
        .background(Color.gray.opacity(0.1))
        // 3. Detectar el toque y cambiar el estado
        .onTapGesture {
            isInputActive = false
        }
    }
}

Manejando Múltiples Campos con Enum

Si tienes un formulario largo con muchos campos (Nombre, Apellido, Dirección), usar un booleano para cada uno es tedioso. @FocusState permite usar tipos Hashable (como un Enum).

struct MultiFieldForm: View {
    enum Field: Hashable {
        case firstName
        case lastName
        case email
    }

    @State private var firstName = ""
    @State private var lastName = ""
    @State private var email = ""
    
    @FocusState private var focusedField: Field?

    var body: some View {
        VStack {
            TextField("Nombre", text: $firstName)
                .focused($focusedField, equals: .firstName)
            
            TextField("Apellido", text: $lastName)
                .focused($focusedField, equals: .lastName)
            
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)
        }
        .padding()
        .onTapGesture {
            // Asignar nil descarta el teclado
            focusedField = nil
        }
    }
}

Método 3: Listas y Formularios (iOS 16+)

A partir de iOS 16, SwiftUI introdujo un modificador específico para ScrollViewList y Form que imita el comportamiento nativo de aplicaciones como WhatsApp o Mensajes, donde el teclado se baja al hacer scroll.

Este modificador es .scrollDismissesKeyboard.

struct CommentSection: View {
    @State private var comment: String = ""

    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<20) { i in
                    Text("Comentario #\(i)")
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding()
                }
            }
        }
        // Opciones: .immediately, .interactively, .never
        .scrollDismissesKeyboard(.interactively)
        
        // Área de entrada en la parte inferior
        HStack {
            TextField("Escribe un comentario...", text: $comment)
                .textFieldStyle(.roundedBorder)
            Button("Enviar") { }
        }
        .padding()
    }
}

Nota Importante: Este método funciona mientras se hace scroll. Si el usuario simplemente toca el espacio vacío sin deslizar, el teclado podría permanecer visible. Por eso, a menudo se combina este método con el gesto de toque.


El Problema del “Espacio Vacío” (Troubleshooting)

Un error muy común que cometen los desarrolladores junior en SwiftUI es aplicar un .onTapGesture a un contenedor como un VStack y descubrir que no funciona cuando tocan en los espacios en blanco entre los elementos.

¿Por qué sucede esto?

Por defecto, en SwiftUI, los contenedores como VStack o HStack solo son “táctiles” donde tienen contenido. El espacio vacío entre dos textos es, literalmente, vacío (transparente a los toques). El toque pasa a través de ellos y no se registra.

La Solución: contentShape

Para arreglar esto, debes definir una forma de contenido para el área de toque, o añadir un fondo.

Opción A: Añadir un fondo transparente

VStack {
    // contenido
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white) // O Color.clear si es sobre otro fondo
.onTapGesture {
    // cerrar teclado
}

Opción B: Usar contentShape (Más elegante)

VStack {
    // contenido
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle()) // Hace que toda el área sea interactiva
.onTapGesture {
    // cerrar teclado
}

El uso de .contentShape(Rectangle()) es fundamental cuando quieres detectar toques en áreas que parecen vacías pero que conceptualmente forman parte de tu fondo.


UX Avanzada: Añadiendo una Barra de Herramientas

A veces, ocultar el teclado tocando afuera no es suficiente, especialmente en teclados numéricos (.keyboardType(.numberPad)) que no tienen una tecla de “Return” o “Done” por defecto. Para una experiencia de usuario (UX) pulida, debes agregar una barra de herramientas.

struct AmountView: View {
    @State private var amount: String = ""
    @FocusState private var isFocused: Bool

    var body: some View {
        TextField("Cantidad", text: $amount)
            .keyboardType(.decimalPad)
            .focused($isFocused)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer() // Empuja el botón a la derecha
                    
                    Button("Hecho") {
                        isFocused = false
                    }
                }
            }
    }
}

Este código añade una pequeña barra sobre el teclado con un botón “Hecho”, lo cual es una práctica estándar en iOS para teclados numéricos.


Creando un Modificador Reutilizable

Para evitar repetir el código de onTapGesture y UIApplication.shared... en cada vista, lo ideal es crear un ViewModifierpersonalizado. Esto mantiene tu código limpio y legible.

struct HideKeyboardOnTap: ViewModifier {
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                UIApplication.shared.sendAction(
                    #selector(UIResponder.resignFirstResponder),
                    to: nil,
                    from: nil,
                    for: nil
                )
            }
    }
}

extension View {
    func hideKeyboardWhenTappedAround() -> some View {
        modifier(HideKeyboardOnTap())
    }
}

Uso:

var body: some View {
    VStack {
        // Tu formulario
    }
    .hideKeyboardWhenTappedAround()
}

Consideraciones de Accesibilidad y iPadOS

Al diseñar la interacción de ocultar el teclado, no olvides a los usuarios que no utilizan la app de manera convencional.

  1. VoiceOver: Los usuarios que utilizan lectores de pantalla pueden no “tocar fuera” de la misma manera. Asegúrate de que siempre haya una forma explícita de cerrar el teclado o finalizar la edición (como el botón “Done” en la toolbar o la tecla “Return” en el teclado).
  2. iPad y Teclados Físicos: En iPad, muchos usuarios tienen teclados externos. Ellos esperan que la tecla CMD + . o Esc descarte ciertas interacciones. FocusState suele manejar bien la pérdida de foco, pero probar tu app con un teclado físico conectado es un paso de QA vital.

Conclusión

Gestionar el teclado en SwiftUI ha evolucionado desde los trucos de UIApplication hasta el manejo elegante de estado con @FocusState.

Para una aplicación moderna (iOS 15/16+), la recomendación de oro es:

  1. Usa @FocusState para controlar la lógica de qué campo está activo.
  2. Usa .scrollDismissesKeyboard(.interactively) en cualquier lista o formulario con scroll.
  3. Añade un .onTapGesture en el fondo (asegurándote de usar .contentShape(Rectangle())) que establezca tu variable de foco a nil o false.

Al combinar estas técnicas, aseguras que tu aplicación no solo se vea bien, sino que se sienta robusta, nativa y respetuosa con la experiencia del usuario.

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 cambiar el color de un placeholder en un TextField en SwiftUI

Next Article

Cómo ocultar el teclado en SwiftUI

Related Posts