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:
onTapGesturepuede 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 deUIApplication, 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
- Declaramos la variable de estado de foco.
- Vinculamos el
TextFielda esa variable. - Cambiamos la variable a
falsecuando 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 ScrollView, List 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.
- 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).
- iPad y Teclados Físicos: En iPad, muchos usuarios tienen teclados externos. Ellos esperan que la tecla
CMD + .oEscdescarte ciertas interacciones.FocusStatesuele 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:
- Usa
@FocusStatepara controlar la lógica de qué campo está activo. - Usa
.scrollDismissesKeyboard(.interactively)en cualquier lista o formulario con scroll. - Añade un
.onTapGestureen el fondo (asegurándote de usar.contentShape(Rectangle())) que establezca tu variable de foco anilofalse.
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









