16 déc. 2025·8 min de lecture

Validation de formulaires SwiftUI qui semble native : focus et erreurs

Validation de formulaires SwiftUI qui semble native : gérer le focus, afficher des erreurs inline au bon moment et présenter clairement les messages serveur sans agacer les utilisateurs.

Validation de formulaires SwiftUI qui semble native : focus et erreurs

À quoi ressemble une validation « native » dans SwiftUI

Un formulaire iOS qui paraît natif est calme. Il ne dispute pas l'utilisateur pendant qu'il tape. Il donne un retour clair quand c'est utile, et il ne vous fait pas chercher ce qui a mal tourné.

L'attente principale est la prévisibilité. Les mêmes actions doivent produire le même type de retour à chaque fois. Si un champ est invalide, le formulaire doit l'indiquer au même endroit, avec le même ton, et proposer une étape suivante claire.

La plupart des formulaires finissent par avoir trois types de règles :

  • Règles de champ : cette valeur seule est-elle valide (vide, format, longueur) ?
  • Règles inter-champs : les valeurs correspondent-elles ou dépendent-elles l'une de l'autre (Mot de passe et Confirmation) ?
  • Règles serveur : le backend l'accepte-t-il (email déjà utilisé, invitation requise) ?

Le timing compte plus que des formulations astucieuses. Une bonne validation attend un moment significatif, puis s'exprime une fois, clairement. Un rythme pratique ressemble à ceci :

  • Rester silencieux pendant que l'utilisateur tape, surtout pour les règles de format.
  • Montrer un retour après avoir quitté un champ, ou après que l'utilisateur a appuyé sur Soumettre.
  • Garder les erreurs visibles jusqu'à ce qu'elles soient corrigées, puis les retirer immédiatement.

La validation doit être discrète tant que l'utilisateur forme encore sa réponse, comme en train de saisir un email ou un mot de passe. Afficher une erreur au premier caractère donne l'impression de ronchonner, même si c'est techniquement correct.

La validation doit devenir visible quand l'utilisateur signale qu'il a fini : le focus quitte le champ, ou il essaie de soumettre. C'est le moment où il veut des indications, et c'est là que vous pouvez l'aider à atteindre le champ exact qui demande une correction.

Bien gérer le timing facilite tout le reste. Les messages inline peuvent rester courts, les mouvements de focus semblent aidants, et les erreurs côté serveur paraissent normales plutôt que punitives.

Mettez en place un modèle d'état de validation simple

Un formulaire au look natif commence par une séparation claire : le texte que l'utilisateur a tapé n'est pas la même chose que l'opinion de l'app à propos de ce texte. Si vous les mélangez, vous afficherez les erreurs trop tôt ou vous perdrez les messages serveur quand l'UI se rafraîchit.

Une approche simple est de donner à chaque champ son propre état composé de quatre éléments : la valeur courante, si l'utilisateur a interagi, l'erreur locale (sur l'appareil), et l'erreur serveur (le cas échéant). Ensuite l'UI peut décider quoi afficher en se basant sur « touché » et « soumis », au lieu de réagir à chaque frappe.

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()
}

Quelques petites règles gardent tout cela prévisible :

  • Gardez séparées les erreurs locales et serveur. Les règles locales (comme « requis » ou « email invalide ») ne doivent pas écraser un message serveur comme « email déjà utilisé ».
  • Effacez serverError quand l'utilisateur modifie à nouveau ce champ, pour qu'il ne reste pas bloqué devant un message ancien.
  • Ne mettez touched = true que lorsque l'utilisateur quitte le champ (ou quand vous décidez qu'il a essayé d'interagir), pas au premier caractère saisi.

Avec ça en place, votre vue peut se lier librement à value. La validation met à jour localError, et votre couche API définit serverError, sans qu'ils se battent entre eux.

Gestion du focus qui guide, pas qui harcèle

Une bonne validation SwiftUI donne l'impression que le clavier système aide l'utilisateur à finir une tâche, pas que l'app lui fait la leçon. Le focus joue un grand rôle là-dedans.

Un modèle simple consiste à traiter le focus comme une source unique de vérité en utilisant @FocusState. Définissez un enum pour vos champs, liez chaque champ à cet enum, puis passez au suivant quand l'utilisateur appuie sur le bouton du clavier.

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 }

Ce qui rend cela naturel, c'est la retenue. Ne déplacez le focus que sur des actions claires de l'utilisateur : appuyer sur Next, Done, ou le bouton principal. Au moment du submit, placez le focus sur le premier champ invalide (et faites défiler jusqu'à lui si nécessaire). Ne dérobez pas le focus pendant que l'utilisateur tape, même si la valeur est actuellement invalide. Restez aussi cohérent avec les labels du clavier : Next pour les champs intermédiaires, Done pour le dernier champ.

Un exemple courant est l'inscription. L'utilisateur appuie sur Créer un compte. Vous validez une fois, affichez les erreurs, puis placez le focus sur le premier champ en erreur (souvent Email). S'il est en train de taper le mot de passe, ne le ramenez pas à Email en plein milieu d'une frappe. Ce petit détail fait souvent la différence entre un « formulaire iOS soigné » et un « formulaire agaçant ».

Erreurs inline qui apparaissent au bon moment

Les erreurs inline doivent ressembler à un conseil discret, pas à une réprimande. La plus grande différence entre « natifs » et « agaçants » est précisément le moment où vous montrez le message.

Règles de timing

Si une erreur apparaît dès que quelqu'un commence à taper, elle interrompt. Une meilleure règle est : attendez que l'utilisateur ait eu une chance raisonnable de finir le champ.

Des bons moments pour révéler une erreur inline :

  • Après que le champ a perdu le focus
  • Après que l'utilisateur a tapé Soumettre
  • Après une courte pause pendant la saisie (seulement pour des vérifications évidentes, comme le format d'email)

Une approche fiable est de n'afficher un message que lorsque le champ est touché ou quand une tentative de soumission a eu lieu. Un formulaire frais reste calme, mais l'utilisateur reçoit des indications claires dès qu'il interagit.

Mise en page et style

Rien n'est moins iOS-like que la mise en page qui saute quand une erreur apparaît. Réservez de l'espace pour le message, ou animez son apparition pour qu'il ne pousse pas abruptement le champ suivant.

Gardez le texte d'erreur court et précis, avec une correction par message. « Le mot de passe doit faire au moins 8 caractères » est exploitable. « Entrée invalide » ne l'est pas.

Pour le style, visez la subtilité et la cohérence. Une petite police sous le champ (comme une note de bas de page), une couleur d'erreur uniforme et un léger surlignage du champ lisent généralement mieux que des arrière-plans lourds. Effacez le message dès que la valeur redevient valide.

Un exemple réaliste : sur un formulaire d'inscription, n'affichez pas « Email invalide » tant que l'utilisateur tape name@. Montrez-le après qu'il quitte le champ, ou après une brève pause, et supprimez-le dès que l'adresse devient valide.

Flux de validation locale : saisie, sortie d'un champ, soumission

Générez l'app et le backend
Modélisez les utilisateurs et les identifiants dans PostgreSQL, puis générez l'app et le backend ensemble.
Démarrer le projet

Un bon flux local a trois vitesses : des indices discrets pendant la saisie, des vérifications plus strictes quand on quitte un champ, et des règles complètes à la soumission. C'est ce rythme qui rend la validation naturelle.

Pendant que l'utilisateur tape, gardez la validation légère et discrète. Pensez « est-ce manifestement impossible ? » plutôt que « est-ce parfait ? ». Pour un champ email, vous ne vérifierez peut-être que la présence d'un @ et l'absence d'espaces. Pour un mot de passe, vous pouvez montrer un petit aide-mémoire comme « 8+ caractères » une fois qu'il a commencé à taper, mais évitez les erreurs en rouge au premier caractère.

Quand l'utilisateur quitte un champ, exécutez des règles plus strictes sur le champ et affichez les erreurs inline si nécessaire. C'est l'endroit pour « Requis » et « Format invalide ». C'est aussi un bon moment pour supprimer les espaces en début/fin et normaliser l'entrée (par exemple mettre l'email en minuscules) afin que l'utilisateur voie ce qui sera soumis.

À la soumission, validez tout de nouveau, y compris les règles inter-champs que vous ne pouvez pas décider avant. L'exemple classique est Mot de passe et Confirmation. Si cela échoue, placez le focus sur le champ qui demande correction et affichez un message clair à côté.

Utilisez le bouton de soumission avec soin. Gardez-le activé pendant que l'utilisateur remplit le formulaire. Désactivez-le seulement si appuyer ne ferait rien (par exemple, pendant l'envoi). Si vous le désactivez pour une entrée invalide, montrez quand même quoi corriger à proximité.

Pendant la soumission, montrez un état de chargement clair. Remplacez le label du bouton par un ProgressView, empêchez les doubles taps et laissez le formulaire visible pour que l'utilisateur comprenne ce qui se passe. Si la requête prend plus d'une seconde, un petit texte comme « Création du compte... » réduit l'anxiété sans ajouter de bruit.

Validation côté serveur sans frustrer les utilisateurs

Les vérifications serveur sont la source de vérité finale, même si vos contrôles locaux sont solides. Un mot de passe peut passer vos règles mais échouer parce qu'il est trop courant, ou un email peut déjà être pris.

Le plus grand gain UX est de séparer « votre saisie n'est pas acceptable » de « nous n'avons pas pu joindre le serveur ». Si la requête échoue ou que l'utilisateur est hors ligne, ne marquez pas les champs comme invalides. Affichez une bannière calme ou une alerte du type « Impossible de se connecter. Réessayez. » et conservez le formulaire tel quel.

Quand le serveur dit que la validation a échoué, gardez la saisie de l'utilisateur intacte et pointez vers les champs concernés. Effacer le formulaire, vider un mot de passe ou déplacer le focus donne l'impression de punir l'utilisateur qui a essayé.

Un schéma simple est d'analyser une réponse d'erreur structurée en deux catégories : erreurs par champ et erreurs globales. Puis mettez à jour votre état UI sans modifier les bindings de texte.

struct ServerValidation: Decodable {
  var fieldErrors: [String: String]
  var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.

Ce qui semble généralement natif :

  • Placez les messages de champ inline, sous le champ, en utilisant la formulation serveur quand elle est claire.
  • Placez le focus sur le premier champ en erreur seulement après une soumission, pas en plein milieu de la saisie.
  • Si le serveur retourne plusieurs problèmes, affichez le premier par champ pour garder la lisibilité.
  • Si vous avez des détails par champ, n'utilisez pas « Quelque chose s'est mal passé. » en guise de repli.

Exemple : l'utilisateur soumet un formulaire d'inscription et le serveur renvoie « email déjà utilisé ». Conservez l'email saisi, affichez le message sous Email et placez le focus sur ce champ. Si le serveur est indisponible, montrez un message unique de retry et laissez tous les champs tels quels.

Comment afficher les messages serveur au bon endroit

Standardisez les modèles de validation
Créez des états d'erreur et des flux d'envoi cohérents sans réécrire la logique pour chaque écran.
Construire maintenant

Les erreurs serveur paraissent « injustes » si elles apparaissent dans une bannière aléatoire. Placez chaque message le plus près possible du champ qui l'a causé. Utilisez un message général seulement quand vous ne pouvez vraiment pas l'attacher à un champ précis.

Commencez par traduire la charge d'erreur du serveur en identifiants SwiftUI de vos champs. Le backend peut renvoyer des clés comme email, password ou profile.phone, tandis que votre UI utilise un enum comme Field.email et Field.password. Faites ce mapping une fois, juste après la réponse, afin que le reste de votre vue reste cohérent.

Une manière flexible de modéliser cela est de garder serverFieldErrors: [Field: [String]] et serverFormErrors: [String]. Stockez des tableaux même si vous n'affichez généralement qu'un message. Quand vous affichez une erreur inline, choisissez d'abord le message le plus utile. Par exemple, « Email déjà utilisé » est plus utile que « Email invalide » si les deux apparaissent.

Plusieurs erreurs par champ sont courantes, mais en montrer toutes est bruyant. La plupart du temps, n'affichez que le premier message inline et conservez les autres pour une vue détaillée si nécessaire.

Pour les erreurs non attachées à un champ (session expirée, limites de fréquence, « Réessayez plus tard »), placez-les près du bouton de soumission afin que l'utilisateur les voie au moment d'agir. Assurez-vous aussi d'effacer les anciennes erreurs en cas de succès pour que l'UI ne donne pas l'impression d'être « bloquée ».

Enfin, effacez les erreurs serveur quand l'utilisateur modifie le champ lié. En pratique, un handler onChange pour email devrait retirer serverFieldErrors[.email] pour que l'UI reflète immédiatement « d'accord, vous êtes en train de corriger ça ».

Accessibilité et ton : petits choix qui paraissent natifs

Du no-code au code
Obtenez le code source prêt pour la production pour iOS, Android, web et backend quand vous en avez besoin.
Générer le code

Une bonne validation ne concerne pas uniquement la logique. C'est aussi la manière dont ça se lit, se prononce et se comporte avec le Texte Dynamique, VoiceOver et différentes langues.

Rendre les erreurs faciles à lire (et pas seulement avec la couleur)

Supposez que le texte peut être grand. Utilisez des styles compatibles Dynamic Type (comme .font(.footnote) ou .font(.caption) sans tailles fixes), et laissez les labels d'erreur se répartir sur plusieurs lignes. Gardez un espacement cohérent pour que la mise en page ne saute pas trop quand une erreur apparaît.

Ne comptez pas uniquement sur le rouge. Ajoutez une icône claire, un préfixe « Erreur : » ou les deux. Cela aide les personnes ayant des problèmes de vision des couleurs et accélère le balayage visuel.

Un rapide ensemble de vérifications qui tient généralement la route :

  • Utilisez un style de texte lisible qui s'adapte à Dynamic Type.
  • Autorisez le retour à la ligne et évitez la troncature des messages d'erreur.
  • Ajoutez une icône ou un label comme « Erreur : » en plus de la couleur.
  • Gardez un contraste élevé en Mode Clair et Mode Sombre.

Faire en sorte que VoiceOver lise la bonne chose

Quand un champ est invalide, VoiceOver doit lire le label, la valeur courante et l'erreur ensemble. Si l'erreur est un Text séparé sous le champ, elle peut être sautée ou lue hors contexte.

Deux approches aident :

  • Combinez le champ et son erreur en un seul élément d'accessibilité, pour que l'erreur soit annoncée quand l'utilisateur focusse le champ.
  • Définissez un hint ou une valeur d'accessibilité qui inclut le message d'erreur (par exemple, « Mot de passe, requis, doit contenir au moins 8 caractères »).

Le ton compte aussi. Rédigez des messages clairs et faciles à localiser. Évitez l'argot, les blagues et les lignes vagues comme « Oups ». Préférez des consignes précises comme « Email manquant » ou « Le mot de passe doit inclure un chiffre ».

Exemple : un formulaire d'inscription avec règles locales et serveur

Imaginez un formulaire d'inscription avec trois champs : Email, Mot de passe et Confirmation du mot de passe. L'objectif est un formulaire qui reste silencieux pendant la saisie, puis devient utile quand l'utilisateur veut avancer.

Ordre de focus (ce que fait Return)

Avec SwiftUI FocusState, chaque pression sur Return doit ressembler à une étape naturelle.

  • Return sur Email : passer au Mot de passe.
  • Return sur Mot de passe : passer à Confirmation.
  • Return sur Confirmation : fermer le clavier et tenter la soumission.
  • Si la soumission échoue : placez le focus sur le premier champ qui nécessite une correction.

Cette dernière étape est importante. Si l'email est invalide, le focus retourne sur Email, pas seulement un message rouge quelque part.

Quand les erreurs apparaissent

Une règle simple garde l'UI calme : afficher les messages après qu'un champ est touché (l'utilisateur l'a quitté) ou après une tentative de soumission.

  • Email : afficher « Saisissez un email valide » après avoir quitté le champ, ou lors de la soumission.
  • Mot de passe : afficher les règles (comme longueur minimale) après avoir quitté, ou lors de la soumission.
  • Confirmation : afficher « Les mots de passe ne correspondent pas » après avoir quitté, ou lors de la soumission.

Passons au côté serveur. Supposons que l'utilisateur soumette et que votre API renvoie quelque chose comme :

{
  "errors": {
    "email": "That email is already in use.",
    "password": "Password is too weak. Try 10+ characters."
  }
}

Ce que l'utilisateur voit : Email affiche le message serveur juste en dessous, et Mot de passe affiche son message sous Mot de passe. Confirmation reste silencieuse sauf si elle échoue localement.

Ce qu'il fait ensuite : le focus se place sur Email (la première erreur serveur). Il change l'email, appuie sur Return pour passer à Mot de passe, ajuste le mot de passe, puis soumet à nouveau. Parce que les messages sont inline et que le focus bouge avec intention, le formulaire paraît coopératif, pas moralisateur.

Pièges courants qui donnent un rendu « non iOS » à la validation

Gérez proprement les erreurs serveur
Testez les erreurs côté serveur comme email déjà pris sans effacer les saisies utilisateur ni casser l'interface.
Prototyper

Un formulaire peut être techniquement correct et pourtant sembler faux. La plupart des problèmes « non iOS » viennent du timing : quand vous affichez une erreur, quand vous déplacez le focus, et comment vous réagissez au serveur.

Une erreur fréquente est de parler trop tôt. Si vous affichez une erreur au premier caractère, les gens se sentent réprimandés pendant qu'ils tapent. Attendre que le champ soit touché (qu'ils le quittent ou qu'ils essaient de soumettre) règle généralement cela.

Les réponses serveur asynchrones peuvent aussi casser le flux. Si une requête d'inscription revient et que vous sautez soudainement le focus vers un autre champ, ça semble aléatoire. Gardez le focus là où l'utilisateur était, et ne le bougez que quand il le demande ou quand vous gérez une soumission.

Un autre piège est d'effacer tout à chaque édition. Supprimer toutes les erreurs dès qu'un caractère change peut cacher le vrai problème, surtout pour les messages serveur. Effacez seulement l'erreur du champ en cours d'édition, et conservez les autres jusqu'à ce qu'ils soient réellement corrigés.

Évitez les boutons Soumettre qui « échouent silencieusement ». Désactiver Soumettre sans expliquer quoi corriger force l'utilisateur à deviner. Si vous le désactivez, accompagnez d'indices précis, ou autorisez la soumission puis guidez vers le premier problème.

Les requêtes lentes et les taps en double sont faciles à manquer. Si vous ne montrez pas de progression et n'empêchez pas les doubles envois, l'utilisateur tapera deux fois, recevra deux réponses et se retrouvera avec des erreurs confuses.

Voici une petite checklist de bon sens :

  • Retardez les erreurs jusqu'au blur ou à la soumission, pas au premier caractère.
  • Ne déplacez pas le focus après une réponse serveur sauf si l'utilisateur l'a demandé.
  • Effacez les erreurs par champ, pas tout d'un coup.
  • Expliquez pourquoi Soumettre est bloqué (ou autorisez la soumission et guidez).
  • Montrez le chargement et ignorez les taps supplémentaires en attendant.

Exemple : si le serveur dit « email déjà utilisé » (peut-être depuis un backend généré avec AppMaster), gardez le message sous Email, laissez Mot de passe intact, et permettez à l'utilisateur d'éditer Email sans relancer tout le formulaire.

Checklist rapide et prochaines étapes

Une validation qui paraît native tient surtout au timing et à la retenue. Vous pouvez avoir des règles strictes et faire en sorte que l'écran reste calme.

Avant de publier, vérifiez :

  • Validez au bon moment. N'affichez pas d'erreurs au premier caractère sauf si c'est clairement utile.
  • Déplacez le focus avec un but. À la soumission, placez le focus sur le premier champ invalide et montrez clairement ce qui ne va pas.
  • Gardez les formulations courtes et précises. Dites quoi faire ensuite, pas seulement ce que l'utilisateur a « mal fait ».
  • Respectez le chargement et les retries. Désactivez le bouton de soumission pendant l'envoi, et conservez les valeurs saisies si la requête échoue.
  • Traitez les erreurs serveur comme des retours de champ quand c'est possible. Mappez les codes serveur à un champ, et n'utilisez un message général que pour les problèmes vraiment globaux.

Ensuite, testez comme une vraie personne. Tenez un petit téléphone dans une main et essayez de remplir le formulaire avec votre pouce. Ensuite activez VoiceOver et vérifiez que l'ordre du focus, l'annonce des erreurs et les labels des boutons restent cohérents.

Pour le débogage et le support, il aide de logger les codes de validation serveur (pas les messages bruts) en plus de l'écran et du nom du champ. Quand un utilisateur dit « je n'arrive pas à m'inscrire », vous pouvez rapidement voir si c'était email_taken, weak_password ou un timeout réseau.

Pour garder ça cohérent dans toute l'app, standardisez votre modèle de champ (value, touched, local error, server error), le placement des erreurs et les règles de focus. Si vous voulez créer des formulaires iOS natifs plus vite sans tout coder à la main, AppMaster (appmaster.io) peut générer des apps SwiftUI avec des services backend, ce qui facilite l'alignement des règles côté client et côté serveur.

Facile à démarrer
Créer quelque chose d'incroyable

Expérimentez avec AppMaster avec un plan gratuit.
Lorsque vous serez prêt, vous pourrez choisir l'abonnement approprié.

Démarrer