Validação de formulários SwiftUI com sensação nativa: foco e erros
Validação de formulários SwiftUI com sensação nativa: gerencie o foco, mostre erros inline no momento certo e exiba mensagens do servidor com clareza, sem irritar os usuários.

Como é uma validação com “sensação nativa” no SwiftUI
Um formulário iOS com sensação nativa é calmo. Ele não fica brigando com o usuário enquanto ele digita. Dá um feedback claro quando importa, e não faz você caçar o que deu errado.
A principal expectativa é previsibilidade. As mesmas ações devem levar ao mesmo tipo de feedback toda vez. Se um campo está inválido, o formulário deve mostrar isso num lugar consistente, com um tom consistente e com um próximo passo claro.
A maioria dos formulários acaba precisando de três tipos de regras:
- Regras por campo: Esse único valor é válido (vazio, formato, comprimento)?
- Regras entre campos: Os valores batem ou dependem um do outro (Senha e Confirmar Senha)?
- Regras do servidor: O backend aceita isso (email já usado, convite obrigatório)?
O timing importa mais do que frases espertas. Uma boa validação espera por um momento significativo e fala uma vez, com clareza. Um ritmo prático fica assim:
- Fique em silêncio enquanto o usuário digita, especialmente para regras de formato.
- Mostre feedback após sair do campo ou depois que o usuário tocar em Enviar.
- Mantenha os erros visíveis até serem corrigidos, então remova-os imediatamente.
A validação deve ficar silenciosa enquanto o usuário ainda está formando a resposta, como ao digitar um email ou senha. Mostrar um erro no primeiro caractere soa como cobrança, mesmo que esteja tecnicamente correto.
A validação deve ficar visível quando o usuário sinaliza que terminou: o foco sai do campo, ou ele tenta submeter. Esse é o momento em que ele quer orientação, e é quando você pode ajudá-lo a ir direto ao campo que precisa de atenção.
Acertar o timing facilita todo o resto. Mensagens inline podem ser curtas, o movimento de foco parece útil e os erros do servidor passam a ser feedback normal ao invés de punição.
Configure um modelo simples de estado de validação
Um formulário com sensação nativa começa com uma separação clara: o texto que o usuário digitou não é a mesma coisa que a opinião do app sobre esse texto. Se você misturar, vai ou mostrar erros cedo demais ou perder mensagens do servidor quando a UI for atualizada.
Uma abordagem simples é dar a cada campo seu próprio estado com quatro partes: o valor atual, se o usuário interagiu com ele, o erro local (no dispositivo) e o erro do servidor (se houver). Então a UI pode decidir o que mostrar com base em “tocado” e “submetido”, ao invés de reagir a cada pressionar de tecla.
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()
}
Algumas regras pequenas mantêm isso previsível:
- Mantenha erros locais e de servidor separados. Regras locais (como “obrigatório” ou “email inválido”) não devem sobrescrever uma mensagem do servidor como “email já cadastrado”.
- Limpe
serverErrorquando o usuário editar novamente esse campo, para que ele não fique preso olhando uma mensagem antiga. - Só configure
touched = truequando o usuário sair do campo (ou quando você decidir que ele tentou interagir), não no primeiro caractere digitado.
Com isso em prática, sua view pode fazer bind no value livremente. A validação atualiza localError e a camada de API define serverError, sem que um sobrescreva o outro.
Tratamento de foco que guia, não que cobra
Uma boa validação em SwiftUI deve parecer que o teclado do sistema ajuda o usuário a terminar uma tarefa, não que o app o repreende. O foco é grande parte disso.
Um padrão simples é tratar o foco como uma única fonte de verdade usando @FocusState. Defina um enum para seus campos, vincule cada campo a ele e avance quando o usuário tocar no botão do 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 }
O que mantém isso com sensação nativa é a contenção. Mova o foco apenas em ações claras do usuário: tocar em Next, Done ou no botão primário. Ao submeter, foque o primeiro campo inválido (e role até ele se necessário). Não roube o foco enquanto o usuário está digitando, mesmo que o valor esteja atualmente inválido. Também mantenha consistência nos rótulos do teclado: Next para campos intermediários, Done para o último campo.
Um exemplo comum é Cadastro. O usuário toca em Criar Conta. Você valida uma vez, mostra erros e então foca o primeiro campo com falha (frequentemente Email). Se ele estiver no campo Senha e ainda digitando, não volte ele para Email no meio da digitação. Esse pequeno detalhe muitas vezes faz a diferença entre “formulário iOS bem polido” e “formulário irritante”.
Erros inline que aparecem na hora certa
Erros inline devem parecer uma dica discreta, não uma repreensão. A maior diferença entre “nativo” e “irritante” é quando você mostra a mensagem.
Regras de timing
Se um erro aparece no momento em que alguém começa a digitar, ele interrompe. Uma regra melhor é: espere até o usuário ter uma chance justa de terminar o campo.
Bons momentos para revelar um erro inline:
- Depois que o campo perde o foco
- Depois que o usuário toca em Enviar
- Após uma pausa curta enquanto digita (apenas para checagens óbvias, como formato de email)
Uma abordagem confiável é mostrar uma mensagem somente quando o campo foi tocado ou quando houve uma tentativa de submissão. Um formulário novo permanece calmo, mas o usuário recebe orientação clara assim que interage.
Layout e estilo
Nada parece menos iOS do que o layout pulando quando um erro aparece. Reserve espaço para a mensagem ou anime sua aparência para não empurrar abruptamente o próximo campo para baixo.
Mantenha o texto de erro curto e específico, com uma correção por mensagem. “Senha deve ter pelo menos 8 caracteres” é acionável. “Entrada inválida” não é.
Para estilo, opte por algo sutil e consistente. Uma fonte pequena sob o campo (como footnote), uma cor de erro consistente e um destaque leve no campo geralmente funcionam melhor do que fundos pesados. Limpe a mensagem assim que o valor ficar válido.
Um exemplo realista: num formulário de cadastro, não mostre “Email inválido” enquanto o usuário ainda está digitando nome@. Mostre após ele sair do campo ou depois de uma breve pausa, e remova a mensagem no momento em que o endereço se tornar válido.
Fluxo de validação local: digitando, saindo do campo, submetendo
Um bom fluxo local tem três velocidades: dicas suaves enquanto digita, checagens mais firmes ao sair do campo e regras completas ao submeter. Esse ritmo é o que faz a validação parecer nativa.
Enquanto o usuário digita, mantenha a validação leve e discreta. Pense “isso é claramente impossível?” e não “isso está perfeito?”. Para um campo de email, você pode apenas checar que contém @ e não tem espaços. Para uma senha, pode mostrar um pequeno helper como “8+ caracteres” assim que ele começar a digitar, mas evite erros em vermelho no primeiro caractere.
Quando o usuário sai de um campo, rode regras mais estritas por campo e mostre erros inline se necessário. É aqui que “Obrigatório” e “Formato inválido” pertencem. Também é um bom momento para aparar espaços e normalizar a entrada (como deixar o email em minúsculas) para que o usuário veja o que será enviado.
Ao submeter, valide tudo de novo, incluindo regras entre campos que você não pode decidir antes. O exemplo clássico é Senha e Confirmar Senha baterem. Se isso falhar, foque o campo que precisa ser corrigido e mostre uma mensagem clara perto dele.
Use o botão de envio com cuidado. Mantenha-o habilitado enquanto o usuário ainda preenche o formulário. Desabilite-o apenas quando o toque realmente não fizer nada (por exemplo, enquanto já estiver enviando). Se você o desabilitar por entrada inválida, mostre o que corrigir por perto.
Durante o envio, mostre um estado de carregamento claro. Troque o rótulo do botão por um ProgressView, evite toques duplos e mantenha o formulário visível para que o usuário entenda o que está acontecendo. Se a requisição demorar mais de um segundo, um rótulo curto como “Criando conta...” reduz a ansiedade sem adicionar ruído.
Validação do servidor sem frustrar usuários
As checagens do servidor são a fonte final de verdade, mesmo que suas checagens locais sejam fortes. Uma senha pode passar nas suas regras, mas falhar por ser muito comum, ou um email pode já estar em uso.
O maior ganho de UX é separar “sua entrada não é aceitável” de “não conseguimos alcançar o servidor”. Se a requisição expirar ou o usuário estiver offline, não marque campos como inválidos. Mostre um banner calmo ou um alerta como “Não foi possível conectar. Tente novamente.” e mantenha o formulário exatamente como está.
Quando o servidor diz que a validação falhou, mantenha a entrada do usuário intacta e aponte para os campos exatos. Limpar o formulário, apagar uma senha ou mover o foco faz as pessoas se sentirem punidas por tentar.
Um padrão simples é parsear uma resposta de erro estruturada em dois buckets: erros por campo e erros de nível de formulário. Então atualize o estado da UI sem mudar os bindings de texto.
struct ServerValidation: Decodable {
var fieldErrors: [String: String]
var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.
O que geralmente parece nativo:
- Coloque mensagens de campo inline, sob o campo, usando a redação do servidor quando ela for clara.
- Mova o foco para o primeiro campo com erro apenas depois de uma tentativa de envio, não no meio da digitação.
- Se o servidor retornar múltiplos problemas, mostre o primeiro por campo para manter legibilidade.
- Se você tiver detalhes por campo, não caia no genérico “Algo deu errado.”
Exemplo: o usuário submete um formulário de cadastro e o servidor retorna “email já em uso”. Mantenha o email que ele digitou, mostre a mensagem sob Email e foque nesse campo. Se o servidor estiver fora, mostre uma única mensagem de retry e deixe todos os campos como estavam.
Como exibir mensagens do servidor no lugar certo
Erros do servidor parecem “injustos” quando aparecem num banner aleatório. Coloque cada mensagem o mais próximo possível do campo que a causou. Use uma mensagem geral apenas quando você realmente não conseguir vinculá-la a uma entrada.
Comece traduzindo o payload de erro do servidor para os identificadores de campo do seu SwiftUI. O backend pode retornar chaves como email, password ou profile.phone, enquanto sua UI usa um enum como Field.email e Field.password. Faça esse mapeamento uma vez, logo após a resposta, para que o resto da view continue consistente.
Uma forma flexível de modelar isso é manter serverFieldErrors: [Field: [String]] e serverFormErrors: [String]. Armazene arrays mesmo que você geralmente mostre uma mensagem. Quando exibir um erro inline, escolha a mensagem mais útil primeiro. Por exemplo, “Email já em uso” é mais útil que “Email inválido” se ambos aparecerem.
Erros múltiplos por campo são comuns, mas mostrar todos é barulhento. Na maioria das vezes, mostre apenas a primeira mensagem inline e mantenha o restante para uma view de detalhes se realmente precisar.
Para erros que não estão ligados a nenhum campo (sessão expirada, limite de taxa, “Tente novamente mais tarde”), posicione-os perto do botão de envio para que o usuário os veja no momento da ação. Também limpe erros antigos ao ter sucesso para que a UI não pareça “travada”.
Finalmente, limpe erros do servidor quando o usuário alterar o campo relacionado. Na prática, um onChange para email deve remover serverFieldErrors[.email] para que a UI reflita imediatamente “OK, você está corrigindo”.
Acessibilidade e tom: pequenas escolhas que parecem nativas
Boa validação não é só lógica. É também sobre como ela é lida, soada e como se comporta com Dynamic Type, VoiceOver e diferentes idiomas.
Torne erros fáceis de ler (e não só com cor)
Suponha que o texto possa ficar grande. Use estilos compatíveis com Dynamic Type (como .font(.footnote) ou .font(.caption) sem tamanhos fixos) e deixe rótulos de erro quebrarem linha. Mantenha espaçamento consistente para que o layout não pule demais quando um erro aparecer.
Não dependa apenas do vermelho. Adicione um ícone claro, um prefixo “Erro:” ou ambos. Isso ajuda pessoas com dificuldades de visão de cor e acelera a leitura.
Um conjunto rápido de checagens que geralmente funciona:
- Use um estilo de texto legível que escale com Dynamic Type.
- Permita quebra de linha e evite truncamento para mensagens de erro.
- Adicione um ícone ou rótulo como “Erro:” junto com a cor.
- Mantenha alto contraste em Light Mode e Dark Mode.
Faça o VoiceOver ler a coisa certa
Quando um campo é inválido, o VoiceOver deve ler o rótulo, o valor atual e o erro juntos. Se o erro for um Text separado abaixo do campo, ele pode ser pulado ou lido fora de contexto.
Duas práticas ajudam:
- Combine o campo e seu erro em um único elemento de acessibilidade, para que o erro seja anunciado quando o usuário focar o campo.
- Defina um hint ou value de acessibilidade que inclua a mensagem de erro (por exemplo, “Senha, obrigatório, deve ter pelo menos 8 caracteres”).
O tom também importa. Escreva mensagens claras e fáceis de localizar. Evite gírias, piadas e linhas vagas como “Ops”. Prefira orientações específicas como “Falta o email” ou “Senha deve incluir um número”.
Exemplo: um formulário de cadastro com regras locais e do servidor
Imagine um formulário de cadastro com três campos: Email, Password e Confirm Password. O objetivo é um formulário que fica quieto enquanto o usuário digita e passa a ser útil quando ele tenta avançar.
Ordem do foco (o que Return faz)
Com SwiftUI FocusState, cada pressionar de Return deve parecer um passo natural.
- Return em Email: mover o foco para Password.
- Return em Password: mover o foco para Confirm Password.
- Return em Confirm Password: fechar o teclado e tentar Enviar.
- Se o envio falhar: mover o foco para o primeiro campo que precisa de atenção.
Esse último passo importa. Se o email for inválido, o foco volta para Email, não apenas para uma mensagem vermelha em algum lugar.
Quando aparecem erros
Uma regra simples mantém a UI calma: mostrar mensagens após um campo ser tocado (o usuário sai dele) ou após uma tentativa de envio.
- Email: mostrar “Digite um email válido” após sair do campo ou no envio.
- Password: mostrar regras (como comprimento mínimo) após sair ou no envio.
- Confirm Password: mostrar “Senhas não coincidem” após sair ou no envio.
Agora o lado do servidor. Suponha que o usuário envie e sua API retorne algo como:
{
"errors": {
"email": "That email is already in use.",
"password": "Password is too weak. Try 10+ characters."
}
}
O que o usuário vê: Email mostra a mensagem do servidor bem abaixo dele, e Password mostra sua mensagem abaixo de Password. Confirm Password fica quieto, a menos que também falhe localmente.
O que ele faz em seguida: o foco vai para Email (o primeiro erro do servidor). Ele muda o email, pressiona Return para ir para Password, ajusta a senha e submete de novo. Como as mensagens são inline e o foco se move com propósito, o formulário parece cooperativo, não punitivo.
Armadilhas comuns que fazem a validação parecer “não iOS”
Um formulário pode estar tecnicamente correto e ainda assim soar errado. A maioria dos problemas “não iOS” vem de timing: quando você mostra um erro, quando move o foco e como reage ao servidor.
Um erro comum é falar cedo demais. Se você mostra um erro no primeiro caractere, as pessoas se sentem repreendidas durante a digitação. Esperar até o campo ser tocado (sair dele, ou tentar submeter) geralmente resolve isso.
Respostas assíncronas do servidor também podem quebrar o fluxo. Se uma requisição de cadastro retorna e você pula o foco para outro campo, parece aleatório. Mantenha o foco onde o usuário estava por último e mova-o apenas quando ele pedir (Next) ou quando você estiver lidando com um envio.
Outra armadilha é limpar tudo a cada edição. Limpar todos os erros assim que qualquer caractere muda pode esconder o problema real, especialmente com mensagens do servidor. Limpe apenas o erro do campo sendo editado e mantenha o resto até que sejam realmente corrigidos.
Evite botões de envio que “falham silenciosamente”. Desabilitar Enviar para sempre sem explicar o que falta força o usuário a adivinhar. Se desabilitar, emparelhe com dicas específicas, ou permita o envio e então guie para o primeiro problema.
Requisições lentas e toques duplicados são fáceis de esquecer. Se você não mostrar progresso e prevenir envios duplicados, o usuário tocará duas vezes, receberá duas respostas e ficará confuso.
Aqui está uma checagem rápida:
- Adie erros até blur ou submit, não até o primeiro caractere.
- Não mova o foco após uma resposta do servidor a não ser que o usuário tenha pedido.
- Limpe erros por campo, não tudo de uma vez.
- Explique porque o envio está bloqueado (ou permita enviar com orientação).
- Mostre carregamento e ignore toques extras enquanto espera.
Exemplo: se o servidor diz “email já em uso” (talvez vindo de um backend que você construiu no AppMaster), mantenha a mensagem sob Email, mantenha Password intacto e deixe o usuário editar Email sem reiniciar todo o formulário.
Checklist rápido e próximos passos
Uma experiência de validação com sensação nativa é, na maior parte, sobre timing e contenção. Você pode ter regras rígidas e ainda fazer a tela parecer calma.
Antes de lançar, verifique:
- Valide no momento certo. Não mostre erros no primeiro caractere, a menos que seja realmente útil.
- Mova o foco com propósito. Ao submeter, salte para o primeiro campo inválido e deixe claro o que corrigir.
- Mantenha as mensagens curtas e específicas. Diga o que fazer a seguir, não apenas o que o usuário fez “de errado”.
- Respeite carregamento e tentativas. Desative o botão de envio enquanto envia e mantenha os valores digitados se a requisição falhar.
- Trate erros do servidor como feedback de campo quando possível. Mapeie códigos do servidor para um campo e use uma mensagem superior apenas para problemas verdadeiramente globais.
Depois, teste como uma pessoa real. Segure um celular pequeno numa mão e tente completar o formulário com o polegar. Depois ative o VoiceOver e verifique se a ordem de foco, anúncios de erro e rótulos de botão continuam coerentes.
Para depuração e suporte, ajuda registrar códigos de validação do servidor (não mensagens brutas) junto com a tela e o nome do campo. Quando um usuário diz “não consigo me cadastrar”, você pode rapidamente identificar se foi email_taken, weak_password ou um timeout de rede.
Para manter isso consistente em todo um app, padronize seu modelo de campo (value, touched, local error, server error), o posicionamento de erros e as regras de foco. Se quiser criar formulários iOS nativos mais rápido sem codificar cada tela à mão, o AppMaster (appmaster.io) pode gerar apps SwiftUI junto com serviços de backend, o que facilita alinhar regras de validação no cliente e no servidor.


