SwiftUI form validation that feels native: focus and errors
SwiftUI form validation that feels native: handle focus, show inline errors at the right time, and display server messages clearly without annoying users.

What ânative-feelingâ validation looks like in SwiftUI
A native-feeling iOS form is calm. It doesnât argue with the user while they type. It gives clear feedback when it matters, and it doesnât make you hunt for what went wrong.
The main expectation is predictability. The same actions should lead to the same kind of feedback every time. If a field is invalid, the form should show it in a consistent place, with a consistent tone, and with a clear next step.
Most forms end up needing three kinds of rules:
- Field rules: Is this single value valid (empty, format, length)?
- Cross-field rules: Do values match or depend on each other (Password and Confirm Password)?
- Server rules: Does the backend accept it (email already used, invite required)?
Timing matters more than clever wording. Good validation waits for a meaningful moment, then speaks once, clearly. A practical rhythm looks like this:
- Stay quiet while the user is typing, especially for format rules.
- Show feedback after leaving a field, or after the user taps Submit.
- Keep errors visible until fixed, then remove them immediately.
Validation should be silent while the user is still forming the answer, like typing an email or password. Showing an error on the first character feels like nagging, even if itâs technically correct.
Validation should become visible when the user signals theyâre done: focus moves away, or they try to submit. Thatâs the moment they want guidance, and thatâs when you can help them land on the exact field that needs attention.
Get the timing right and everything else gets easier. Inline messages can stay short, focus movement feels helpful, and server-side errors feel like normal feedback instead of punishment.
Set up a simple validation state model
A native-feeling form starts with a clean separation: the text the user typed isnât the same thing as the appâs opinion about that text. If you mix them, youâll either show errors too early or lose server messages when the UI refreshes.
A simple approach is to give each field its own state with four parts: the current value, whether the user has interacted with it, the local (on-device) error, and the server error (if any). Then the UI can decide what to show based on âtouchedâ and âsubmittedâ, instead of reacting to every keystroke.
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()
}
A few small rules keep this predictable:
- Keep local and server errors separate. Local rules (like ârequiredâ or âinvalid emailâ) shouldnât overwrite a server message like âemail already takenâ.
- Clear
serverErrorwhen the user edits that field again, so they arenât stuck staring at an old message. - Only set
touched = truewhen the user leaves the field (or when you decide they tried to interact), not on the first character typed.
With this in place, your view can bind to value freely. Validation updates localError, and your API layer sets serverError, without them fighting each other.
Focus handling that guides, not nags
Good SwiftUI validation should feel like the system keyboard is helping the user finish a task, not like the app is scolding them. Focus is a big part of that.
A simple pattern is to treat focus as a single source of truth using @FocusState. Define an enum for your fields, bind each field to it, then move forward when the user taps the keyboard button.
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 }
What keeps this feeling native is restraint. Move focus only on clear user actions: tapping Next, Done, or the primary button. On submit, focus the first invalid field (and scroll to it if needed). Donât steal focus while the user is typing, even if the value is currently invalid. Also stay consistent with keyboard labels: Next for intermediate fields, Done for the last field.
A common example is Sign Up. The user taps Create Account. You validate once, show errors, then set focus to the first failing field (often Email). If theyâre in the Password field and still typing, donât jump them back to Email mid-keystroke. That small detail is often the difference between âpolished iOS formâ and âannoying formâ.
Inline errors that appear at the right time
Inline errors should feel like a quiet hint, not a scolding. The biggest difference between ânativeâ and âannoyingâ is when you show the message.
Timing rules
If an error shows the moment someone starts typing, it interrupts. A better rule is: wait until the user has had a fair chance to finish the field.
Good moments to reveal an inline error:
- After the field loses focus
- After the user taps Submit
- After a short pause while typing (only for obvious checks, like email format)
A reliable approach is to show a message only when the field is touched or when submit was attempted. A fresh form stays calm, but the user still gets clear guidance once they interact.
Layout and style
Nothing feels less iOS-like than the layout jumping when an error appears. Reserve space for the message, or animate its appearance so it doesnât shove the next field down abruptly.
Keep error text short and specific, with one fix per message. âPassword must be at least 8 charactersâ is actionable. âInvalid inputâ isnât.
For styling, aim for subtle and consistent. A small font under the field (like a footnote), one consistent error color, and a gentle highlight on the field usually reads better than heavy backgrounds. Clear the message as soon as the value becomes valid.
A realistic example: on a signup form, donât show âEmail is invalidâ while the user is still typing name@. Show it after they leave the field, or after a brief pause, and remove it the moment the address becomes valid.
Local validation flow: typing, leaving a field, submitting
A good local flow has three speeds: gentle hints while typing, firmer checks when you leave a field, and full rules when you submit. That rhythm is what makes validation feel native.
While the user types, keep validation lightweight and quiet. Think âis this obviously impossible?â not âis this perfect?â For an email field, you might only check that it contains @ and no spaces. For a password, you might show a small helper like â8+ charactersâ once theyâve started typing, but avoid red errors on the first keystroke.
When the user leaves a field, run stricter single-field rules and show inline errors if needed. This is where âRequiredâ and âInvalid formatâ belong. Itâs also a good moment to trim whitespace and normalize input (like lowercasing an email) so the user sees what will be submitted.
On submit, validate everything again, including cross-field rules you canât decide earlier. The classic example is Password and Confirm Password matching. If this fails, move focus to the field that needs fixing and show one clear message near it.
Use the submit button carefully. Keep it enabled while the user is still filling out the form. Disable it only when tapping would do nothing (for example, while already submitting). If you do disable it for invalid input, still show what to fix nearby.
During submission, show a clear loading state. Swap the button label for a ProgressView, prevent double taps, and keep the form visible so users understand whatâs happening. If the request takes longer than a second, a short label like âCreating account...â reduces anxiety without adding noise.
Server-side validation without frustrating users
Server-side checks are the final source of truth, even if your local checks are strong. A password might pass your rules but fail because itâs too common, or an email might already be taken.
The biggest UX win is to separate âyour input isnât acceptableâ from âwe couldnât reach the server.â If the request times out or the user is offline, donât mark fields as invalid. Show a calm banner or alert like âCouldnât connect. Try again.â and keep the form exactly as it is.
When the server says validation failed, keep the userâs input intact and point to the exact fields. Wiping the form, clearing a password, or moving focus away makes people feel punished for trying.
A simple pattern is to parse a structured error response into two buckets: field errors and form-level errors. Then update your UI state without changing the text bindings.
struct ServerValidation: Decodable {
var fieldErrors: [String: String]
var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.
What usually feels native:
- Put field messages inline, under the field, using the serverâs wording when itâs clear.
- Move focus to the first field with an error only after submit, not mid-typing.
- If the server returns multiple issues, show the first one per field to keep it readable.
- If you have field details, donât fall back to âSomething went wrong.â
Example: the user submits a signup form, and the server returns âemail already in use.â Keep the email they typed, show the message under Email, and focus that field. If the server is down, show a single retry message and leave all fields alone.
How to display server messages in the right place
Server errors feel âunfairâ when they show up in a random banner. Put each message as close as possible to the field that caused it. Use a general message only when you truly canât tie it to a single input.
Start by translating the serverâs error payload into your SwiftUI field identifiers. The backend might return keys like email, password, or profile.phone, while your UI uses an enum like Field.email and Field.password. Do the mapping once, right after the response, so the rest of your view can stay consistent.
A flexible way to model this is to keep serverFieldErrors: [Field: [String]] and serverFormErrors: [String]. Store arrays even if you usually show one message. When you do show an inline error, pick the most helpful message first. For example, âEmail already in useâ is more useful than âInvalid emailâ if both appear.
Multiple errors per field are common, but showing all of them is noisy. Most of the time, show only the first message inline and keep the rest for a details view if you truly need it.
For errors that arenât tied to a field (expired session, rate limits, âTry again laterâ), place them near the submit button so the user sees them right when they act. Also make sure you clear old errors on success so the UI doesnât look âstuckâ.
Finally, clear server errors when the user changes the related field. In practice, an onChange handler for email should remove serverFieldErrors[.email] so the UI immediately reflects, âOkay, youâre fixing it.â
Accessibility and tone: small choices that feel native
Good validation isnât only about logic. Itâs also about how it reads, sounds, and behaves with Dynamic Type, VoiceOver, and different languages.
Make errors easy to read (and not only with color)
Assume text can get large. Use Dynamic Type-friendly styles (like .font(.footnote) or .font(.caption) without fixed sizes), and let error labels wrap. Keep spacing consistent so the layout doesnât jump too much when an error appears.
Donât rely on red text alone. Add a clear icon, an âError:â prefix, or both. This helps people with color vision issues and makes scanning faster.
A quick set of checks that usually holds up:
- Use a readable text style that scales with Dynamic Type.
- Allow wrapping and avoid truncation for error messages.
- Add an icon or label like âError:â along with color.
- Keep contrast high in both Light Mode and Dark Mode.
Make VoiceOver read the right thing
When a field is invalid, VoiceOver should read the label, the current value, and the error together. If the error is a separate Text below the field, it can be skipped or read out of context.
Two patterns help:
- Combine the field and its error into one accessibility element, so the error is announced when the user focuses the field.
- Set an accessibility hint or value that includes the error message (for example, âPassword, required, must be at least 8 charactersâ).
Tone matters, too. Write messages that are clear and easy to localize. Avoid slang, jokes, and vague lines like âOopsâ. Prefer specific guidance like âEmail is missingâ or âPassword must include a numberâ.
Example: a signup form with both local and server rules
Imagine a signup form with three fields: Email, Password, and Confirm Password. The goal is a form that stays quiet while the user is typing, then becomes helpful when they try to move forward.
Focus order (what Return does)
With SwiftUI FocusState, each Return key press should feel like a natural step.
- Email Return: move focus to Password.
- Password Return: move focus to Confirm Password.
- Confirm Password Return: dismiss the keyboard and attempt Submit.
- If Submit fails: move focus to the first field that needs attention.
That last step matters. If the email is invalid, focus goes back to Email, not just to a red message somewhere else.
When errors appear
A simple rule keeps the UI calm: show messages after a field is touched (the user leaves it) or after a submit attempt.
- Email: show âEnter a valid emailâ after leaving the field, or on Submit.
- Password: show rules (like minimum length) after leaving, or on Submit.
- Confirm Password: show âPasswords donât matchâ after leaving, or on Submit.
Now the server side. Suppose the user submits and your API returns something like:
{
"errors": {
"email": "That email is already in use.",
"password": "Password is too weak. Try 10+ characters."
}
}
What the user sees: Email shows the server message right under it, and Password shows its message under Password. Confirm Password stays quiet unless it also fails locally.
What they do next: focus lands on Email (the first server error). They change the email, press Return to move to Password, adjust the password, then submit again. Because the messages are inline and the focus moves with intent, the form feels cooperative, not scolding.
Common traps that make validation feel âun-iOSâ
A form can be technically correct and still feel wrong. Most âun-iOSâ validation problems come down to timing: when you show an error, when you move focus, and how you react to the server.
A common mistake is speaking too early. If you show an error on the first keystroke, people feel scolded while typing. Waiting until the field is touched (they leave it, or they try to submit) usually fixes that.
Async server responses can also break the flow. If a signup request returns and you suddenly jump focus to another field, it feels random. Keep focus where the user last was, and only move it when they tap Next or when youâre handling a submit attempt.
Another trap is wiping the slate clean on every edit. Clearing all errors as soon as any character changes can hide the real problem, especially with server messages. Clear only the error for the field being edited, and keep the rest until theyâre actually fixed.
Avoid âsilent failureâ submit buttons. Disabling Submit forever without explaining what to fix forces users to guess. If you disable it, pair it with specific hints, or allow submit and then guide them to the first issue.
Slow requests and duplicate taps are easy to miss. If you donât show progress and prevent double submits, users will tap twice, get two responses, and end up with confusing errors.
Hereâs a quick sanity check:
- Delay errors until blur or submit, not the first character.
- Donât move focus after a server response unless the user asked.
- Clear errors per field, not everything at once.
- Explain why submit is blocked (or allow submit with guidance).
- Show loading and ignore extra taps while waiting.
Example: if the server says âemail already in useâ (maybe from a backend you built in AppMaster), keep the message under Email, keep Password untouched, and let the user edit Email without restarting the whole form.
Quick checklist and next steps
A native-feeling validation experience is mostly about timing and restraint. You can have strict rules and still make the screen feel calm.
Before you ship, check these:
- Validate at the right time. Donât show errors on the first keystroke unless itâs clearly helpful.
- Move focus with purpose. On submit, jump to the first invalid field and make it obvious whatâs wrong.
- Keep wording short and specific. Say what to do next, not what the user did âwrongâ.
- Respect loading and retries. Disable the submit button while sending, and keep typed values if the request fails.
- Treat server errors as field feedback when possible. Map server codes to a field, and use a top message only for truly global issues.
Then test it like a real person. Hold a small phone in one hand and try to complete the form with your thumb. After that, turn on VoiceOver and make sure focus order, error announcements, and button labels still make sense.
For debugging and support, it helps to log server validation codes (not raw messages) alongside the screen and field name. When a user says âit wonât let me sign upâ, you can quickly tell whether it was email_taken, weak_password, or a network timeout.
To keep this consistent across an app, standardize your field model (value, touched, local error, server error), error placement, and focus rules. If you want to build native iOS forms faster without hand-coding every screen, AppMaster (appmaster.io) can generate SwiftUI apps alongside backend services, which can make it easier to keep client and server validation rules aligned.


