Validación de formularios SwiftUI que se siente nativa: foco y errores
Validación de formularios SwiftUI que se siente nativa: gestiona el foco, muestra errores en línea en el momento adecuado y presenta mensajes del servidor sin molestar al usuario.

Cómo es una validación que “se siente nativa” en SwiftUI
Un formulario iOS que se siente nativo transmite calma. No discute con el usuario mientras escribe. Da retroalimentación clara cuando importa y no obliga a buscar qué salió mal.
La expectativa principal es la predictibilidad. Las mismas acciones deberían producir el mismo tipo de respuesta cada vez. Si un campo es inválido, el formulario debería mostrarlo en un lugar consistente, con un tono coherente y con un paso siguiente claro.
La mayoría de los formularios necesita tres tipos de reglas:
- Reglas de campo: ¿Es válido este valor individual (vacío, formato, longitud)?
- Reglas entre campos: ¿Los valores coinciden o dependen entre sí (Contraseña y Confirmar contraseña)?
- Reglas del servidor: ¿Lo acepta el backend (email ya usado, invitación requerida)?
El momento importa más que las frases ingeniosas. Una buena validación espera un momento significativo y luego habla una sola vez, con claridad. Un ritmo práctico se ve así:
- Mantente en silencio mientras el usuario escribe, especialmente para reglas de formato.
- Muestra retroalimentación al salir de un campo o cuando el usuario pulsa Enviar.
- Mantén los errores visibles hasta que se arreglen, y elimínalos inmediatamente cuando se corrijan.
La validación debe ser silenciosa mientras el usuario aún está formando la respuesta, como al escribir un email o una contraseña. Mostrar un error en el primer carácter se siente como regañar, aunque sea técnicamente correcto.
La validación debe hacerse visible cuando el usuario indica que terminó: el foco se va del campo o intenta enviar. Ese es el momento en el que quieren orientación y en el que puedes ayudarles a ir al campo exacto que necesita atención.
Acierta el timing y todo lo demás es más fácil. Los mensajes en línea pueden ser breves, el movimiento del foco se siente útil y los errores del servidor se perciben como retroalimentación normal en lugar de castigo.
Configura un modelo de estado de validación simple
Un formulario que se siente nativo comienza con una separación clara: el texto que el usuario escribió no es lo mismo que la opinión de la app sobre ese texto. Si los mezclas, mostrarás errores demasiado pronto o perderás mensajes del servidor cuando la UI se refresque.
Un enfoque simple es dar a cada campo su propio estado con cuatro partes: el valor actual, si el usuario ha interactuado con él, el error local (en el dispositivo) y el error del servidor (si lo hay). Entonces la UI puede decidir qué mostrar en base a “touched” y “submitted”, en lugar de reaccionar a cada pulsación.
struct FieldState {
var value: String = ""
var touched: Bool = false
var localError: String? = nil
var serverError: String? = nil
// One source of truth for what the UI displays
func displayedError(submitted: Bool) -> String? {
guard touched || submitted else { return nil }
return localError ?? serverError
}
}
struct FormState {
var submitted: Bool = false
var email = FieldState()
var password = FieldState()
}
Unas pocas reglas pequeñas mantienen esto predecible:
- Mantén separados los errores locales y los del servidor. Las reglas locales (como “requerido” o “email inválido”) no deberían sobrescribir un mensaje del servidor como “email ya registrado”.
- Borra
serverErrorcuando el usuario edite nuevamente ese campo, para que no se quede mirando un mensaje antiguo. - Solo pon
touched = truecuando el usuario salga del campo (o cuando decidas que intentó interactuar), no en el primer carácter.
Con esto en su lugar, tu vista puede enlazarse a value libremente. La validación actualiza localError y la capa de API pone serverError, sin que se peleen entre sí.
Manejo del foco que guía, no que regaña
La buena validación en SwiftUI debe sentirse como el teclado del sistema ayudando al usuario a completar una tarea, no como la app reprendiendo. El foco es una gran parte de eso.
Un patrón simple es tratar el foco como una única fuente de verdad usando @FocusState. Define un enum para tus campos, enlaza cada campo a él y avanza cuando el usuario pulse el botón del teclado.
enum Field: Hashable { case email, password, confirm }
@FocusState private var focused: Field?
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.submitLabel(.next)
.focused($focused, equals: .email)
.onSubmit { focused = .password }
SecureField("Password", text: $password)
.submitLabel(.next)
.focused($focused, equals: .password)
.onSubmit { focused = .confirm }
Lo que mantiene esto nativo es la contención. Mueve el foco solo en acciones claras del usuario: tocar Siguiente, Hecho o el botón principal. Al enviar, enfoca el primer campo inválido (y desplázate a él si hace falta). No robes el foco mientras el usuario está escribiendo, incluso si el valor es actualmente inválido. Mantén consistencia con las etiquetas del teclado: Next para campos intermedios, Done para el último campo.
Un ejemplo común es el registro. El usuario toca Crear cuenta. Validas una vez, muestras errores y entonces pones el foco en el primer campo que falla (a menudo Email). Si estaban en el campo Contraseña y aún escriben, no los regreses a Email en plena pulsación. Ese pequeño detalle suele ser la diferencia entre “formulario iOS pulido” y “formulario molesto”.
Errores en línea que aparecen en el momento adecuado
Los errores en línea deben sentirse como una pista silenciosa, no como una reprimenda. La mayor diferencia entre “nativo” y “molesto” es cuándo muestras el mensaje.
Reglas de tiempo
Si un error aparece justo cuando alguien empieza a escribir, interrumpe. Una regla mejor es: espera a que el usuario tenga una oportunidad razonable de terminar el campo.
Buenos momentos para revelar un error en línea:
- Después de que el campo pierda el foco
- Después de que el usuario pulse Enviar
- Tras una breve pausa mientras escribe (solo para comprobaciones obvias, como el formato de email)
Un enfoque fiable es mostrar un mensaje solo cuando el campo está touched o cuando se intentó enviar (submitted). Un formulario nuevo se mantiene calmado, pero el usuario obtiene orientación clara una vez que interactúa.
Diseño y estilo
Nada se siente menos iOS que el diseño que salta cuando aparece un error. Reserva espacio para el mensaje o anima su aparición para que no empuje bruscamente el siguiente campo.
Mantén el texto de error corto y específico, con una sola solución por mensaje. “La contraseña debe tener al menos 8 caracteres” es accionable. “Entrada inválida” no lo es.
Para el estilo, busca algo sutil y consistente. Una fuente pequeña bajo el campo (como footnote), un color de error consistente y un resaltado suave en el campo suele leerse mejor que fondos pesados. Borra el mensaje tan pronto como el valor vuelva a ser válido.
Un ejemplo realista: en un formulario de registro, no muestres “Email inválido” mientras el usuario sigue escribiendo nombre@. Muéstralo al salir del campo o tras una breve pausa, y elimínalo en cuanto la dirección sea válida.
Flujo de validación local: escribir, salir del campo, enviar
Un buen flujo local tiene tres velocidades: pistas suaves mientras se escribe, comprobaciones más firmes al salir de un campo y reglas completas al enviar. Ese ritmo es lo que hace que la validación se sienta nativa.
Mientras el usuario escribe, mantén la validación ligera y discreta. Piensa en “¿esto es obviamente imposible?” no en “¿esto es perfecto?”. Para un campo de email, quizá solo compruebes que contiene @ y no espacios. Para una contraseña, podrías mostrar un pequeño ayudante como “8+ caracteres” una vez que empiecen a escribir, pero evita errores en rojo en la primera pulsación.
Cuando el usuario sale de un campo, ejecuta reglas más estrictas de un solo campo y muestra errores en línea si es necesario. Aquí pertenecen “Requerido” y “Formato inválido”. También es un buen momento para recortar espacios y normalizar entrada (por ejemplo, pasar el email a minúsculas) para que el usuario vea lo que se enviará.
Al enviar, valida todo de nuevo, incluidas las reglas entre campos que no puedes determinar antes. El ejemplo clásico es Contraseña y Confirmar contraseña coinciden. Si falla, mueve el foco al campo que necesita corrección y muestra un mensaje claro cerca.
Usa el botón de envío con cuidado. Mantenlo habilitado mientras el usuario aún está completando el formulario. Desactívalo solo cuando pulsarlo no haga nada (por ejemplo, mientras ya se está enviando). Si lo desactivas por entrada inválida, muestra igualmente qué arreglar cerca.
Durante el envío, muestra un estado de carga claro. Sustituye la etiqueta del botón por un ProgressView, evita toques dobles y mantén el formulario visible para que el usuario entienda qué ocurre. Si la petición tarda más de un segundo, una etiqueta corta como “Creando cuenta...” reduce la ansiedad sin añadir ruido.
Validación del lado del servidor sin frustrar a los usuarios
Las comprobaciones del servidor son la fuente de verdad final, incluso si tus verificaciones locales son fuertes. Una contraseña puede pasar tus reglas pero fallar porque es demasiado común, o un email puede estar ya registrado.
La mayor ganancia en UX es separar “tu entrada no es aceptable” de “no pudimos conectar con el servidor”. Si la petición falla por tiempo de espera o el usuario está sin conexión, no marques campos como inválidos. Muestra un banner o alerta calmada como “No se pudo conectar. Intenta de nuevo.” y deja el formulario tal cual.
Cuando el servidor dice que la validación falló, mantén la entrada del usuario intacta y apunta a los campos exactos. Borrar el formulario, vaciar una contraseña o mover el foco hace que la gente se sienta castigada por intentarlo.
Un patrón simple es parsear una respuesta de error estructurada en dos cubos: errores por campo y errores a nivel de formulario. Luego actualiza el estado de UI sin cambiar los bindings de texto.
struct ServerValidation: Decodable {
var fieldErrors: [String: String]
var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.
Lo que suele sentirse nativo:
- Coloca los mensajes de campo en línea, debajo del campo, usando el texto del servidor cuando sea claro.
- Mueve el foco al primer campo con error solo después de intentar enviar, no mientras se está escribiendo.
- Si el servidor devuelve múltiples problemas, muestra el primero por campo para mantenerlo legible.
- Si tienes detalles por campo, no recurras a “Algo salió mal.”
Ejemplo: el usuario envía un formulario de registro y el servidor devuelve “email ya en uso”. Mantén el email que escribió, muestra el mensaje debajo de Email y enfoca ese campo. Si el servidor está caído, muestra un único mensaje de reintento y deja todos los campos tal cual.
Cómo mostrar los mensajes del servidor en el lugar correcto
Los errores del servidor se sienten “injustos” cuando aparecen en un banner aleatorio. Coloca cada mensaje lo más cerca posible del campo que lo provocó. Usa un mensaje general solo cuando realmente no puedas relacionarlo con una entrada concreta.
Empieza traduciendo la carga de error del servidor a los identificadores de campo de tu SwiftUI. El backend puede devolver claves como email, password o profile.phone, mientras tu UI usa un enum como Field.email y Field.password. Haz el mapeo una vez, justo después de la respuesta, para que el resto de la vista pueda permanecer consistente.
Una forma flexible de modelarlo es mantener serverFieldErrors: [Field: [String]] y serverFormErrors: [String]. Almacena arrays aunque normalmente muestres uno. Cuando muestres un error en línea, elige primero el mensaje más útil. Por ejemplo, “Email ya en uso” es más útil que “Email inválido” si aparecen ambos.
Varios errores por campo son comunes, pero mostrar todos es ruidoso. La mayoría de las veces muestra solo el primer mensaje en línea y guarda el resto para una vista de detalles si realmente la necesitas.
Para errores que no están ligados a un campo (sesión expirada, límites de tasa, “Intenta más tarde”), colócalos cerca del botón de envío para que el usuario los vea justo cuando actúa. Además, asegúrate de limpiar errores antiguos en caso de éxito para que la UI no parezca “atascada”.
Finalmente, borra los errores del servidor cuando el usuario cambie el campo relacionado. En la práctica, un onChange para email debería eliminar serverFieldErrors[.email] para que la UI refleje inmediatamente “Bien, lo estás arreglando”.
Accesibilidad y tono: pequeñas decisiones que se sienten nativas
La buena validación no es solo lógica. También tiene que ver con cómo se lee, suena y se comporta con Dynamic Type, VoiceOver y diferentes idiomas.
Haz que los errores sean fáciles de leer (no solo con color)
Asume que el texto puede aumentar mucho. Usa estilos compatibles con Dynamic Type (como .font(.footnote) o .font(.caption) sin tamaños fijos) y permite que las etiquetas de error se ajusten en varias líneas. Mantén el espaciado consistente para que el diseño no salte demasiado cuando aparece un error.
No confíes solo en el texto en rojo. Añade un ícono claro, un prefijo “Error:” o ambos. Esto ayuda a personas con problemas de visión de color y acelera el escaneo.
Un chequeo rápido que suele funcionar:
- Usa un estilo de texto legible que escale con Dynamic Type.
- Permite ajuste de línea y evita la truncación en mensajes de error.
- Añade un icono o la etiqueta “Error:” junto con el color.
- Mantén alto contraste en Modo Claro y Oscuro.
Haz que VoiceOver lea lo correcto
Cuando un campo es inválido, VoiceOver debería leer la etiqueta, el valor actual y el error juntos. Si el error es un Text separado debajo del campo, puede saltarlo o leerse fuera de contexto.
Dos patrones ayudan:
- Combina el campo y su error en un solo elemento de accesibilidad, de modo que el error se anuncie cuando el usuario enfoque el campo.
- Establece un hint o value de accesibilidad que incluya el mensaje de error (por ejemplo, “Contraseña, requerida, debe tener al menos 8 caracteres”).
El tono también importa. Escribe mensajes claros y fáciles de localizar. Evita jerga, chistes y frases vagas como “Ups”. Prefiere guías específicas como “Falta el email” o “La contraseña debe incluir un número”.
Ejemplo: un formulario de registro con reglas locales y del servidor
Imagina un formulario de registro con tres campos: Email, Contraseña y Confirmar Contraseña. La meta es un formulario que se mantenga tranquilo mientras el usuario escribe y que se vuelva útil cuando intenten avanzar.
Orden de foco (qué hace Return)
Con SwiftUI FocusState, cada pulsación de Return debería sentirse como un paso natural.
- Return en Email: mover foco a Contraseña.
- Return en Contraseña: mover foco a Confirmar contraseña.
- Return en Confirmar contraseña: ocultar teclado e intentar Enviar.
- Si el envío falla: mover foco al primer campo que necesita atención.
Ese último paso importa. Si el email es inválido, el foco vuelve a Email, no solo a un mensaje rojo en algún lugar.
Cuándo aparecen los errores
Una regla simple mantiene la UI calmada: muestra mensajes después de que un campo sea tocado (el usuario sale de él) o después de un intento de envío.
- Email: muestra “Introduce un email válido” al salir del campo o al enviar.
- Contraseña: muestra reglas (como longitud mínima) al salir o al enviar.
- Confirmar contraseña: muestra “Las contraseñas no coinciden” al salir o al enviar.
Ahora el lado servidor. Supongamos que el usuario envía y tu API devuelve algo como:
{
"errors": {
"email": "That email is already in use.",
"password": "Password is too weak. Try 10+ characters."
}
}
Lo que ve el usuario: Email muestra el mensaje del servidor justo debajo y Contraseña muestra su mensaje. Confirmar contraseña se mantiene en silencio salvo que también falle localmente.
Lo que hacen después: el foco va a Email (el primer error del servidor). Cambian el email, pulsan Return para pasar a Contraseña, ajustan la contraseña y envían de nuevo. Porque los mensajes son en línea y el foco se mueve con intención, el formulario se siente cooperativo, no regañón.
Trampas comunes que hacen que la validación se sienta “no iOS”
Un formulario puede ser técnicamente correcto y aun así sentirse mal. La mayoría de los problemas “no iOS” vienen del timing: cuándo muestras un error, cuándo mueves el foco y cómo reaccionas al servidor.
Un error común es hablar demasiado pronto. Si muestras un error en la primera pulsación, la gente se siente reprendida mientras escribe. Esperar hasta que el campo sea tocado (salir o intentar enviar) suele arreglarlo.
Las respuestas asíncronas del servidor también pueden romper el flujo. Si una petición de registro responde y de pronto saltas el foco a otro campo, se siente aleatorio. Mantén el foco donde el usuario estaba y muévelo solo cuando ellos lo pidan.
Otra trampa es limpiar todo al editar. Borrar todos los errores en cuanto cambia cualquier carácter puede ocultar el problema real, especialmente con mensajes del servidor. Borra solo el error del campo que se está editando y conserva el resto hasta que estén realmente arreglados.
Evita botones de Enviar con “fallo silencioso”. Desactivar Enviar para siempre sin explicar qué arreglar obliga a los usuarios a adivinar. Si lo desactivas, acompáñalo con pistas específicas o permite enviar y luego guía al primer problema.
Las peticiones lentas y los taps duplicados son fáciles de pasar por alto. Si no muestras progreso y previenes envíos dobles, los usuarios tocarán dos veces, recibirán dos respuestas y acabarán con errores confusos.
Aquí tienes una lista de comprobación rápida:
- Retrasa errores hasta blur o submit, no hasta el primer carácter.
- No muevas el foco tras una respuesta del servidor a menos que el usuario lo haya pedido.
- Borra errores por campo, no todo a la vez.
- Explica por qué Enviar está bloqueado (o permite enviar con guía).
- Muestra carga e ignora taps extra mientras esperas.
Ejemplo: si el servidor dice “email ya en uso” (tal vez desde un backend que construiste en AppMaster), mantén el mensaje bajo Email, deja Contraseña intacta y permite que el usuario edite Email sin reiniciar todo el formulario.
Lista rápida y siguientes pasos
Una experiencia de validación que se siente nativa tiene más que ver con el tiempo y la contención. Puedes tener reglas estrictas y aun así hacer que la pantalla se sienta tranquila.
Antes de lanzar, revisa esto:
- Valida en el momento correcto. No muestres errores en la primera pulsación a menos que sea claramente útil.
- Mueve el foco con propósito. Al enviar, salta al primer campo inválido y deja claro qué está mal.
- Mantén el texto breve y específico. Di qué hacer a continuación, no qué hizo mal el usuario.
- Respeta la carga y los reintentos. Desactiva el botón de enviar mientras envías y conserva los valores si la petición falla.
- Trata los errores del servidor como retroalimentación de campo cuando sea posible. Mapea los códigos del servidor a un campo y usa un mensaje superior solo para problemas verdaderamente globales.
Luego pruébalo como una persona real. Sostén un teléfono pequeño en una mano y trata de completar el formulario con el pulgar. Después enciende VoiceOver y asegúrate de que el orden de foco, los anuncios de errores y las etiquetas de los botones sigan teniendo sentido.
Para depuración y soporte, ayuda registrar los códigos de validación del servidor (no los mensajes crudos) junto con la pantalla y el nombre del campo. Cuando un usuario diga “no me deja registrarme”, podrás decir rápido si fue email_taken, weak_password o un timeout de red.
Para mantener esto consistente en toda la app, estandariza tu modelo de campo (value, touched, local error, server error), la ubicación de errores y las reglas de foco. Si quieres construir formularios iOS nativos más rápido sin codificar cada pantalla a mano, AppMaster (appmaster.io) puede generar apps SwiftUI junto con servicios backend, lo que facilita alinear las reglas de validación cliente y servidor.


