SwiftUI-formuliervalidatie die natuurlijk aanvoelt: focus en fouten
SwiftUI-formuliervalidatie die natuurlijk aanvoelt: beheer focus, toon inline-fouten op het juiste moment en geef servermeldingen duidelijk weer zonder gebruikers te irriteren.

Hoe "native-gevoel" validatie eruitziet in SwiftUI
Een formulier dat als native aanvoelt op iOS is rustig. Het corrigeert de gebruiker niet terwijl die typt. Het geeft duidelijke feedback wanneer het ertoe doet en laat je niet zoeken naar wat er misging.
De belangrijkste verwachting is voorspelbaarheid. Dezelfde acties moeten elke keer tot dezelfde soort feedback leiden. Als een veld ongeldig is, moet het formulier dat op een consistente plek laten zien, met een consistente toon en met een duidelijk volgende stap.
De meeste formulieren hebben uiteindelijk drie soorten regels nodig:
- Veldregels: Is deze enkele waarde geldig (leeg, formaat, lengte)?
- Cross-field regels: Zijn waarden op elkaar afgestemd of afhankelijk (Wachtwoord en Bevestig wachtwoord)?
- Serverregels: Accepteert de backend het (e-mail al in gebruik, invite vereist)?
Timing is belangrijker dan slimme bewoordingen. Goede validatie wacht op het juiste moment en spreekt dan één keer, duidelijk. Een praktisch ritme ziet er zo uit:
- Blijf stil terwijl de gebruiker typt, vooral voor formatteringsregels.
- Toon feedback nadat een veld verlaten is, of nadat de gebruiker op Verzenden tapt.
- Houd fouten zichtbaar totdat ze zijn opgelost, en verwijder ze direct daarna.
Validatie moet stil zijn terwijl de gebruiker nog bezig is met het vormen van het antwoord, zoals het typen van een e-mail of wachtwoord. Een fout tonen bij het eerste teken voelt als zeuren, ook al is het technisch correct.
Validatie moet zichtbaar worden wanneer de gebruiker aangeeft dat hij klaar is: de focus verplaatst zich, of ze proberen te verzenden. Dat is het moment waarop ze begeleiding willen, en dat is wanneer je ze kunt helpen precies naar het veld te gaan dat aandacht nodig heeft.
Krijg de timing goed en alles wordt makkelijker. Inline-berichten kunnen kort blijven, focusverplaatsing voelt behulpzaam en serverfouten voelen als normale feedback in plaats van een straf.
Stel een eenvoudige validatiestatusmodel in
Een formulier met native-gevoel begint met een duidelijke scheiding: de tekst die de gebruiker heeft getypt is niet hetzelfde als de mening van de app over die tekst. Als je ze mixt, toon je ofwel te vroeg fouten of raak je serverberichten kwijt wanneer de UI ververst wordt.
Een eenvoudige aanpak is elk veld zijn eigen staat te geven met vier onderdelen: de huidige waarde, of de gebruiker ermee heeft interactie gehad, de lokale (op-apparaat) fout en de serverfout (indien aanwezig). De UI kan dan beslissen wat te laten zien op basis van "touched" en "submitted", in plaats van te reageren op elke toetsaanslag.
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()
}
Een paar kleine regels houden dit voorspelbaar:
- Houd lokale en serverfouten gescheiden. Lokale regels (zoals "verplicht" of "ongeldige e-mail") mogen geen serverbericht zoals "e-mail al in gebruik" overschrijven.
- Maak
serverErrorleeg wanneer de gebruiker dat veld opnieuw bewerkt, zodat ze niet naar een oud bericht blijven staren. - Zet
touched = truealleen wanneer de gebruiker het veld verlaat (of wanneer je besluit dat ze geprobeerd hebben te interacteren), niet bij het eerste getypte teken.
Met dit in plaats kan je view vrij binden aan value. Validatie werkt localError bij, en je API-laag zet serverError, zonder dat ze elkaar bestrijden.
Focus-afhandeling die begeleidt, niet zeurt
Goede SwiftUI-validatie moet voelen alsof het systeemtoetsenbord de gebruiker helpt een taak af te ronden, niet alsof de app hen berispt. Focus is een groot deel daarvan.
Een simpel patroon is focus als één bron van waarheid te behandelen met @FocusState. Definieer een enum voor je velden, bind elk veld eraan en ga door wanneer de gebruiker op de knop van het toetsenbord tikt.
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 }
Wat dit native laat aanvoelen, is terughoudendheid. Verplaats focus alleen bij duidelijke gebruikersacties: op Next, Done of de primaire knop. Bij submit, zet de focus op het eerste ongeldige veld (en scroll er eventueel naar toe). Steel geen focus terwijl de gebruiker typt, ook al is de waarde op dat moment ongeldig. Wees consistent met toetsenbordlabels: Next voor tussenliggende velden, Done voor het laatste veld.
Een veelvoorkomend voorbeeld is Aanmelden. De gebruiker tikt op Account aanmaken. Je valideert één keer, toont fouten en zet vervolgens de focus op het eerste falende veld (vaak Email). Als ze in het wachtwoordveld zitten en nog typen, spring ze dan niet middenin de toetsaanslag terug naar Email. Dat kleine detail is vaak het verschil tussen "gepolijst iOS-formulier" en "irritant formulier".
Inline-fouten die op het juiste moment verschijnen
Inline-fouten moeten aanvoelen als een zachte hint, niet als een berisping. Het grootste verschil tussen "native" en "irritant" is wanneer je het bericht toont.
Timingregels
Als een fout verschijnt op het moment dat iemand begint te typen, onderbreekt dat. Een betere regel is: wacht tot de gebruiker een redelijke kans heeft gehad om het veld af te maken.
Goede momenten om een inline-fout te tonen:
- Nadat het veld de focus heeft verloren
- Nadat de gebruiker op Verzenden heeft gedrukt
- Na een korte pauze tijdens het typen (alleen voor voor de hand liggende checks, zoals e-mailformaat)
Een betrouwbare aanpak is een bericht alleen tonen wanneer het veld aangeraakt is of wanneer submit geprobeerd is. Een nieuw formulier blijft rustig, maar de gebruiker krijgt alsnog duidelijke aanwijzingen zodra ze interacteren.
Layout en stijl
Niets voelt minder iOS-achtig dan een layout die springt wanneer een fout verschijnt. Reserveer ruimte voor het bericht of animeer het verschijnen zodat het niet abrupt het volgende veld naar beneden duwt.
Houd fouttekst kort en specifiek, met één oplossing per bericht. "Wachtwoord moet minimaal 8 tekens zijn" is uitvoerbaar. "Ongeldige invoer" is dat niet.
Voor styling mik op subtiel en consistent. Een klein lettertype onder het veld (zoals een voetnoot), één consistente foutkleur en een zachte markering van het veld leest meestal beter dan zware achtergronden. Maak het bericht schoon zodra de waarde geldig wordt.
Een realistisch voorbeeld: op een aanmeldformulier, toon niet "E-mail is ongeldig" terwijl de gebruiker nog naam@ typt. Toon het nadat ze het veld verlaten, of na een korte pauze, en verwijder het zodra het adres geldig is.
Lokale validatiestroom: typen, veld verlaten, submitten
Een goede lokale stroom heeft drie snelheden: zachte hints tijdens het typen, strengere checks bij het verlaten van een veld en volledige regels bij submit. Dat ritme maakt validatie native.
Terwijl de gebruiker typt, houd validatie licht en rustig. Denk "is dit duidelijk onmogelijk?" niet "is dit perfect?" Voor een e-mailveld controleer je misschien alleen of er een @ in zit en geen spaties. Voor een wachtwoord kun je een klein hulpje tonen zoals "8+ tekens" zodra ze begonnen zijn met typen, maar vermijd rode fouten bij de eerste toetsaanslag.
Wanneer de gebruiker een veld verlaat, voer strengere single-field regels uit en toon inline-fouten indien nodig. Hier horen "Verplicht" en "Ongeldig formaat". Dit is ook een goed moment om witruimte af te knippen en invoer te normaliseren (zoals e-mail naar kleine letters) zodat de gebruiker ziet wat zal worden verzonden.
Bij submit valideer je alles opnieuw, inclusief cross-field regels die je eerder niet kunt beslissen. Het klassieke voorbeeld is Wachtwoord en Bevestig wachtwoord die overeen moeten komen. Als dit faalt, zet focus naar het veld dat moet worden aangepast en toon één duidelijk bericht in de buurt.
Gebruik de submitknop met zorg. Houd hem ingeschakeld terwijl de gebruiker het formulier nog aan het invullen is. Schakel hem alleen uit wanneer tappen niets zou doen (bijvoorbeeld tijdens een actieve submit). Als je hem uitschakelt voor ongeldige invoer, laat dan alsnog zien wat er gerepareerd moet worden in de buurt.
Tijdens het verzenden, toon een duidelijke laadstatus. Vervang het knoplabel door een ProgressView, voorkom dubbele taps en houd het formulier zichtbaar zodat gebruikers begrijpen wat er gebeurt. Als het verzoek langer dan een seconde duurt, vermindert een korte tekst zoals "Account aanmaken..." de onrust zonder ruis toe te voegen.
Server-side validatie zonder gebruikers te frustreren
Server-side checks zijn de ultieme bron van waarheid, zelfs als je lokale checks sterk zijn. Een wachtwoord kan aan je regels voldoen maar falen omdat het te veel voorkomt, of een e-mail kan al in gebruik zijn.
De grootste UX-winst is "jouw invoer is niet acceptabel" scheiden van "we konden de server niet bereiken." Als het verzoek time-out of de gebruiker offline is, markeer velden dan niet als ongeldig. Toon een rustige banner of alert zoals "Kon geen verbinding maken. Probeer het opnieuw." en houd het formulier precies zoals het is.
Wanneer de server zegt dat validatie faalde, laat dan de invoer van de gebruiker intact en wijs naar de exacte velden. Het formulier wissen, een wachtwoord leegmaken of de focus verplaatsen laat mensen zich gestraft voelen voor het proberen.
Een simpel patroon is een gestructureerde foutrespons te parsen in twee bakken: veldfouten en formulierniveau-fouten. Werk dan je UI-state bij zonder de tekstbindings te veranderen.
struct ServerValidation: Decodable {
var fieldErrors: [String: String]
var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.
Wat meestal native aanvoelt:
- Plaats veldmeldingen inline, onder het veld, en gebruik de bewoording van de server wanneer die duidelijk is.
- Zet focus op het eerste veld met een fout pas na submit, niet tijdens typen.
- Als de server meerdere problemen terugstuurt, toon dan per veld de eerste om het leesbaar te houden.
- Als je velddetails hebt, val dan niet terug op "Er is iets misgegaan."
Voorbeeld: de gebruiker verstuurt een aanmeldformulier en de server retourneert "e-mail al in gebruik." Houd de e-mail die ze hebben ingevuld, toon het bericht onder Email en zet de focus op dat veld. Als de server down is, toon een enkel retry-bericht en laat alle velden ongewijzigd.
Hoe serverberichten op de juiste plek te tonen
Serverfouten voelen "oneerlijk" wanneer ze in een willekeurige banner opduiken. Plaats elk bericht zo dicht mogelijk bij het veld dat het veroorzaakte. Gebruik een algemeen bericht alleen wanneer je het echt niet aan één invoer kunt koppelen.
Begin met het vertalen van de foutpayload van de server naar je SwiftUI-veldidentificatoren. De backend kan sleutels teruggeven zoals email, password of profile.phone, terwijl je UI een enum gebruikt zoals Field.email en Field.password. Doe de mapping één keer, direct na de response, zodat de rest van je view consistent kan blijven.
Een flexibele manier om dit te modelleren is serverFieldErrors: [Field: [String]] en serverFormErrors: [String] bij te houden. Sla arrays op, zelfs als je meestal één bericht toont. Wanneer je een inline-fout toont, kies dan het meest behulpzame bericht eerst. Bijvoorbeeld: "E-mail al in gebruik" is nuttiger dan "Ongeldige e-mail" als beide aanwezig zijn.
Meerdere fouten per veld komen vaak voor, maar ze allemaal tonen is luidruchtig. Meestal toon je alleen het eerste bericht inline en bewaar je de rest voor een detailweergave als je die echt nodig hebt.
Voor fouten die niet aan een veld zijn gekoppeld (verlopen sessie, rate limits, "Probeer later opnieuw"), plaats ze bij de submitknop zodat de gebruiker ze direct ziet bij het handelen. Zorg er ook voor dat je oude fouten wist bij succes zodat de UI niet "vast" lijkt.
Ten slotte: wis serverfouten wanneer de gebruiker het gerelateerde veld wijzigt. In de praktijk moet een onChange-handler voor email serverFieldErrors[.email] verwijderen zodat de UI onmiddellijk weergeeft: "Oké, je bent het aan het herstellen."
Toegankelijkheid en toon: kleine keuzes die native aanvoelen
Goede validatie gaat niet alleen over logica. Het gaat ook over hoe het leest, klinkt en zich gedraagt met Dynamic Type, VoiceOver en verschillende talen.
Maak fouten makkelijk leesbaar (en niet alleen met kleur)
Ga ervan uit dat tekst groter kan worden. Gebruik Dynamic Type-vriendelijke stijlen (zoals .font(.footnote) of .font(.caption) zonder vaste maten) en laat foutlabels wrappen. Houd de ruimte consistent zodat de layout niet te veel springt als een fout verschijnt.
Vertrouw niet alleen op rode tekst. Voeg een duidelijk icoon toe, een "Fout:"-prefix of beide. Dit helpt mensen met kleurenblindheid en maakt scannen sneller.
Een korte checklist die meestal goed werkt:
- Gebruik een leesbare tekststijl die schaalt met Dynamic Type.
- Sta wrappen toe en voorkom afsnijden van foutmeldingen.
- Voeg een icoon of label zoals "Fout:" toe naast kleur.
- Zorg voor hoog contrast in zowel Light Mode als Dark Mode.
Zorg dat VoiceOver het juiste voorleest
Wanneer een veld ongeldig is, moet VoiceOver het label, de huidige waarde en de fout samen voorlezen. Als de fout een aparte Text onder het veld is, kan deze worden overgeslagen of uit context worden voorgelezen.
Twee patronen helpen:
- Combineer het veld en de fout in één accessibility-element, zodat de fout wordt aangekondigd wanneer de gebruiker het veld focust.
- Zet een accessibility hint of value die het foutbericht bevat (bijvoorbeeld: "Wachtwoord, verplicht, minstens 8 tekens").
Toon en toonhoogte zijn ook belangrijk. Schrijf berichten die duidelijk en makkelijk te lokaliseren zijn. Vermijd slang, grappen en vage lijnen zoals "Oeps". Gebruik liever specifieke aanwijzingen zoals "E-mail ontbreekt" of "Wachtwoord moet een cijfer bevatten".
Voorbeeld: een aanmeldformulier met lokale en serverregels
Stel je een aanmeldformulier voor met drie velden: Email, Wachtwoord en Bevestig wachtwoord. Het doel is een formulier dat stil blijft terwijl de gebruiker typt en dan behulpzaam wordt als ze verder willen.
Focusvolgorde (wat Return doet)
Met SwiftUI FocusState zou elke Return-toetsdruk als een natuurlijke stap moeten voelen.
- Email Return: verplaats focus naar Wachtwoord.
- Wachtwoord Return: verplaats focus naar Bevestig wachtwoord.
- Bevestig wachtwoord Return: sluit het toetsenbord en probeer te verzenden.
- Als Submit faalt: zet focus op het eerste veld dat aandacht nodig heeft.
Die laatste stap doet ertoe. Als de e-mail ongeldig is, gaat focus terug naar Email, niet alleen naar een rood bericht ergens anders.
Wanneer fouten verschijnen
Een simpele regel houdt de UI kalm: toon berichten nadat een veld is aangeraakt (de gebruiker verlaat het) of nadat een submitpoging is gedaan.
- Email: toon "Voer een geldig e-mailadres in" na het verlaten van het veld of bij Submit.
- Wachtwoord: toon regels (zoals minimale lengte) na het verlaten of bij Submit.
- Bevestig wachtwoord: toon "Wachtwoorden komen niet overeen" na het verlaten of bij Submit.
En dan de serverzijde. Stel dat de gebruiker verzendt en je API het volgende terugstuurt:
{
"errors": {
"email": "That email is already in use.",
"password": "Password is too weak. Try 10+ characters."
}
}
Wat de gebruiker ziet: Email toont het serverbericht direct eronder, en Wachtwoord toont zijn bericht onder Wachtwoord. Bevestig wachtwoord blijft stil tenzij het lokaal ook faalt.
Wat ze daarna doen: focus gaat naar Email (de eerste serverfout). Ze veranderen de e-mail, drukken Return om naar Wachtwoord te gaan, passen het wachtwoord aan en verzenden opnieuw. Omdat berichten inline zijn en de focus doelgericht beweegt, voelt het formulier coöperatief, niet beschuldigend.
Veelvoorkomende valkuilen die validatie "on-iOS" laten voelen
Een formulier kan technisch correct zijn en toch onjuist aanvoelen. De meeste "on-iOS" validatieproblemen komen neer op timing: wanneer je een fout toont, wanneer je focus verplaatst en hoe je op de server reageert.
Een veelgemaakte fout is te vroeg spreken. Als je een fout toont bij de eerste toetsaanslag, voelen mensen zich gecorrigeerd tijdens het typen. Wachten tot het veld is aangeraakt (ze verlaten het of proberen te submitten) lost dat meestal op.
Asynchrone serverresponses kunnen de flow ook breken. Als een aanmeldverzoek terugkomt en je plotseling de focus naar een ander veld springt, voelt dat willekeurig. Houd focus waar de gebruiker het laatst was en verplaats het alleen als ze op Next drukken of als je een submit afhandelt.
Een andere valkuil is alles wissen bij elke bewerking. Alle fouten wissen zodra er een karakter verandert kan het echte probleem verbergen, vooral bij serverberichten. Wis alleen de fout voor het veld dat wordt bewerkt en houd de rest totdat ze echt zijn opgelost.
Vermijd "stille" uitgeschakelde submitknoppen. Submit permanent uitschakelen zonder uit te leggen wat er moet worden hersteld dwingt gebruikers te raden. Als je hem uitschakelt, combineer dat met specifieke hints of sta submit toe en leid ze vervolgens naar het eerste probleem.
Langzame verzoeken en dubbele taps zijn gemakkelijk te missen. Als je geen voortgang toont en dubbele submits niet voorkomt, tikken gebruikers twee keer, krijgen twee responses en eindigen met verwarrende fouten.
Hier is een korte sanity-check:
- Stel fouten uit tot blur of submit, niet tot het eerste teken.
- Verplaats geen focus na een serverresponse tenzij de gebruiker dat vroeg.
- Wis fouten per veld, niet alles tegelijk.
- Leg uit waarom submit geblokkeerd is (of sta submit toe met begeleiding).
- Toon loading en negeer extra taps tijdens het wachten.
Voorbeeld: als de server zegt "e-mail al in gebruik" (misschien van een backend die je in AppMaster hebt gebouwd), houd het bericht onder Email, laat Wachtwoord onaangetast en laat de gebruiker Email aanpassen zonder het hele formulier te herstarten.
Snelle checklist en vervolgstappen
Een validatie-ervaring met native-gevoel draait vooral om timing en terughoudendheid. Je kunt strikte regels hebben en toch het scherm rustig laten voelen.
Voordat je publiceert, controleer het volgende:
- Valideer op het juiste moment. Toon geen fouten bij de eerste toetsaanslag tenzij het echt nuttig is.
- Verplaats focus met een doel. Spring bij submit naar het eerste ongeldige veld en maak duidelijk wat er mis is.
- Houd bewoording kort en specifiek. Zeg wat de volgende stap is, niet wat de gebruiker "verkeerd" deed.
- Respecteer laden en herhaalfuncties. Schakel de submitknop uit tijdens verzenden en behoud ingevulde waarden als het verzoek faalt.
- Behandel serverfouten als veldfeedback wanneer mogelijk. Map servercodes naar een veld en gebruik een topmelding alleen voor echt globale problemen.
Test het daarna als een echt persoon. Houd een kleine telefoon in één hand en probeer het formulier met je duim in te vullen. Zet daarna VoiceOver aan en controleer of focusvolgorde, foutaankondigingen en knoplabels nog steeds logisch zijn.
Voor debugging en support helpt het om servervalidatiecodes (niet ruwe berichten) te loggen samen met scherm- en veldnaam. Wanneer een gebruiker zegt "ik kan me niet aanmelden", weet je snel of het email_taken, weak_password of een netwerk-timeout was.
Om dit consistent te houden in een app, standaardiseer je je veldmodel (value, touched, local error, server error), foutplaatsing en focusregels. Als je native iOS-formulieren sneller wilt bouwen zonder elk scherm handmatig te coderen, kan AppMaster (appmaster.io) SwiftUI-apps genereren naast backendservices, wat het makkelijker maakt client- en servervalidatieregels op één lijn te houden.


