Componentes essenciais de interação no SwiftUI
Em SwiftUI, componentes de interação (controles) são Views que permitem ao usuário alterar valores. A ideia central é: cada controle se liga a um estado (@State) ou a um modelo (@Observable/@StateObject) por meio de Binding. Quando o usuário interage, o valor muda e a interface se atualiza automaticamente.
Nos exemplos a seguir, você vai ver os controles mais comuns e, depois, montar um formulário funcional com validação básica, teclado adequado, foco entre campos e envio.
Toggle
Toggle representa um valor booleano (ligado/desligado). Ele precisa de um Binding<Bool>.
@State private var aceitaTermos = false
var body: some View {
Toggle("Aceito os termos", isOn: $aceitaTermos)
.tint(.blue)
}Boas práticas
- Use rótulos claros (o usuário deve entender o que muda ao ativar/desativar).
- Quando o toggle habilita/desabilita algo, reflita isso visualmente (ex.: desabilitar um botão).
Slider
Slider controla um valor numérico contínuo (geralmente Double). Você pode definir intervalo e passo.
@State private var volume: Double = 0.5
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Volume: \(Int(volume * 100))%")
Slider(value: $volume, in: 0...1, step: 0.05)
}
}Dica
Mostre o valor atual (texto) para dar feedback imediato, principalmente quando o valor não é óbvio.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Stepper
Stepper é ótimo para valores discretos (quantidade, idade, itens). Pode ser usado com Int ou Double.
@State private var quantidade = 1
var body: some View {
Stepper("Quantidade: \(quantidade)", value: $quantidade, in: 1...10)
}Picker
Picker permite escolher uma opção entre várias. O valor selecionado deve ser do mesmo tipo das opções (ex.: enum, String, Int).
Exemplo com enum
enum Plano: String, CaseIterable, Identifiable {
case basico = "Básico"
case pro = "Pro"
case premium = "Premium"
var id: String { rawValue }
}
@State private var plano: Plano = .basico
var body: some View {
Picker("Plano", selection: $plano) {
ForEach(Plano.allCases) { item in
Text(item.rawValue).tag(item)
}
}
}Estilos comuns
.pickerStyle(.menu): compacto, bom para formulários..pickerStyle(.segmented): ótimo para poucas opções (2–4)..pickerStyle(.wheel): comum em telas de seleção mais “iOS clássico”.
DatePicker
DatePicker seleciona datas e/ou horários. Você pode limitar o intervalo e escolher componentes exibidos.
@State private var nascimento = Date()
var body: some View {
DatePicker(
"Data de nascimento",
selection: $nascimento,
in: ...Date(),
displayedComponents: [.date]
)
}Dica
Para cadastro, normalmente faz sentido limitar a data ao passado (...Date()).
TextField e SecureField
TextField é usado para entrada de texto. SecureField é similar, mas oculta o conteúdo (senha). Ambos trabalham com Binding<String>.
@State private var email = ""
@State private var senha = ""
var body: some View {
VStack {
TextField("E-mail", text: $email)
SecureField("Senha", text: $senha)
}
}Placeholder
O primeiro parâmetro (ex.: "E-mail") funciona como placeholder. Em formulários, ele ajuda, mas não substitui um rótulo quando o campo precisa de contexto extra. Em Form, o rótulo pode ser o próprio texto do campo ou um Text ao lado.
Tipos de teclado e conteúdo
Escolher o teclado correto melhora a experiência e reduz erros. Use .keyboardType, .textContentType e .autocapitalization/.textInputAutocapitalization (dependendo da versão do iOS).
- E-mail:
.keyboardType(.emailAddress),.textContentType(.emailAddress), desabilitar autocapitalização. - Telefone:
.keyboardType(.phonePad),.textContentType(.telephoneNumber). - Nome:
.textContentType(.name), capitalização de palavras. - Senha:
.textContentType(.newPassword)ou.password.
TextField("E-mail", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled(true)Validação básica de entrada
Validação básica significa checar regras simples antes de permitir o envio: campos obrigatórios, formato mínimo e consistência (ex.: senha e confirmação). A validação pode ser:
- Em tempo real: conforme o usuário digita (bom para feedback imediato).
- No envio: ao tocar em “Cadastrar” (bom para não “poluir” a tela cedo demais).
Uma abordagem prática é combinar as duas: mostrar erros após a primeira tentativa de envio, e depois atualizar em tempo real.
Exemplos de regras simples
- Nome: pelo menos 2 caracteres.
- E-mail: contém
@e.(validação simples, não perfeita). - Senha: mínimo de 8 caracteres.
- Termos: precisa estar marcado.
Focus: avançar entre campos e controlar o teclado
Com @FocusState, você controla qual campo está ativo. Isso permite:
- Avançar para o próximo campo ao pressionar “Next”.
- Fechar o teclado ao enviar.
- Destacar visualmente o campo com erro (se você quiser).
Você define um enum com os campos e liga cada campo ao foco.
enum CampoFoco: Hashable {
case nome, email, senha, confirmarSenha
}
@FocusState private var foco: CampoFoco?Envio do formulário (submit) e botão desabilitado
Você pode usar:
.submitLabel(.next)e.onSubmitpara navegar entre campos.- Desabilitar o botão de envio quando o formulário é inválido (
.disabled(true/false)). - Mostrar feedback de carregamento (ex.:
ProgressView) durante o envio.
Passo a passo: construindo um formulário de cadastro funcional
O objetivo é criar uma tela com: nome, e-mail, senha, confirmação, data de nascimento, plano, newsletter, um slider de “nível de experiência” e um stepper de “quantidade de dependentes” (exemplo). Também vamos incluir validação, mensagens de erro e botão desabilitado.
Passo 1: criar a View e os estados
import SwiftUI
struct CadastroView: View {
@State private var nome = ""
@State private var email = ""
@State private var senha = ""
@State private var confirmarSenha = ""
@State private var nascimento = Date()
@State private var plano: Plano = .basico
@State private var receberNewsletter = true
@State private var experiencia: Double = 0.0
@State private var dependentes: Int = 0
@State private var tentouEnviar = false
@State private var enviando = false
@State private var mensagemServidor: String? = nil
enum CampoFoco: Hashable { case nome, email, senha, confirmarSenha }
@FocusState private var foco: CampoFoco?
var body: some View {
Form {
secaoDadosPessoais
secaoPreferencias
secaoAcoes
}
.navigationTitle("Cadastro")
}
}Note que usamos Form para organização automática em estilo de formulário. Você também pode usar ScrollView + VStack se quiser controle total do layout.
Passo 2: definir o enum do plano
enum Plano: String, CaseIterable, Identifiable {
case basico = "Básico"
case pro = "Pro"
case premium = "Premium"
var id: String { rawValue }
}Passo 3: criar validações como propriedades computadas
Centralizar regras em propriedades computadas deixa o código mais legível e evita duplicação.
extension CadastroView {
var nomeValido: Bool { nome.trimmingCharacters(in: .whitespacesAndNewlines).count >= 2 }
var emailValido: Bool {
let e = email.trimmingCharacters(in: .whitespacesAndNewlines)
return e.contains("@") && e.contains(".") && e.count >= 5
}
var senhaValida: Bool { senha.count >= 8 }
var senhasIguais: Bool { !senha.isEmpty && senha == confirmarSenha }
var termosAceitos: Bool { true } // se você adicionar Toggle de termos, ligue aqui
var formularioValido: Bool {
nomeValido && emailValido && senhaValida && senhasIguais
}
}Se você quiser incluir “Aceitar termos”, crie um @State private var aceitaTermos e use no formularioValido.
Passo 4: montar a seção de dados pessoais com teclado, foco e submit
extension CadastroView {
var secaoDadosPessoais: some View {
Section("Dados") {
TextField("Nome", text: $nome)
.textContentType(.name)
.textInputAutocapitalization(.words)
.focused($foco, equals: .nome)
.submitLabel(.next)
.onSubmit { foco = .email }
if tentouEnviar && !nomeValido {
Text("Informe um nome com pelo menos 2 caracteres.")
.font(.footnote)
.foregroundStyle(.red)
}
TextField("E-mail", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled(true)
.focused($foco, equals: .email)
.submitLabel(.next)
.onSubmit { foco = .senha }
if tentouEnviar && !emailValido {
Text("Digite um e-mail válido.")
.font(.footnote)
.foregroundStyle(.red)
}
SecureField("Senha (mín. 8)", text: $senha)
.textContentType(.newPassword)
.focused($foco, equals: .senha)
.submitLabel(.next)
.onSubmit { foco = .confirmarSenha }
if tentouEnviar && !senhaValida {
Text("A senha deve ter pelo menos 8 caracteres.")
.font(.footnote)
.foregroundStyle(.red)
}
SecureField("Confirmar senha", text: $confirmarSenha)
.textContentType(.newPassword)
.focused($foco, equals: .confirmarSenha)
.submitLabel(.done)
.onSubmit { enviar() }
if tentouEnviar && !senhasIguais {
Text("As senhas não coincidem.")
.font(.footnote)
.foregroundStyle(.red)
}
DatePicker(
"Nascimento",
selection: $nascimento,
in: ...Date(),
displayedComponents: [.date]
)
}
}
}Repare no padrão: campo + mensagem de erro condicional. O tentouEnviar evita mostrar erros antes do usuário tentar enviar.
Passo 5: montar a seção de preferências com Toggle, Picker, Slider e Stepper
extension CadastroView {
var secaoPreferencias: some View {
Section("Preferências") {
Picker("Plano", selection: $plano) {
ForEach(Plano.allCases) { item in
Text(item.rawValue).tag(item)
}
}
.pickerStyle(.menu)
Toggle("Receber newsletter", isOn: $receberNewsletter)
VStack(alignment: .leading, spacing: 8) {
Text("Experiência com iOS: \(Int(experiencia))")
Slider(value: $experiencia, in: 0...10, step: 1)
}
.padding(.vertical, 4)
Stepper("Dependentes: \(dependentes)", value: $dependentes, in: 0...10)
}
}
}Passo 6: ações, feedback ao usuário e botão desabilitado
Aqui entram boas práticas importantes: desabilitar envio quando inválido, mostrar carregamento e exibir mensagens de retorno.
extension CadastroView {
var secaoAcoes: some View {
Section {
if let mensagemServidor {
Text(mensagemServidor)
.font(.footnote)
.foregroundStyle(.secondary)
}
Button {
enviar()
} label: {
HStack {
if enviando {
ProgressView()
}
Text(enviando ? "Enviando..." : "Cadastrar")
}
}
.disabled(enviando || !formularioValido)
}
}
func enviar() {
tentouEnviar = true
mensagemServidor = nil
guard formularioValido else {
// Move o foco para o primeiro campo inválido
if !nomeValido { foco = .nome }
else if !emailValido { foco = .email }
else if !senhaValida { foco = .senha }
else if !senhasIguais { foco = .confirmarSenha }
return
}
foco = nil
enviando = true
// Simulação de envio (substitua por sua chamada de rede)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
enviando = false
mensagemServidor = "Cadastro validado localmente e pronto para envio ao servidor."
}
}
}Organização visual e legibilidade do formulário
Use seções com rótulos claros
Separar em Section (“Dados”, “Preferências”, “Ações”) reduz carga cognitiva e melhora a navegação visual.
Evite excesso de mensagens de erro
- Mostre erros apenas quando necessário (ex.: após tentativa de envio).
- Prefira mensagens específicas e curtas.
- Se o erro for global (ex.: falha no servidor), mostre em um local consistente (topo da seção de ações, por exemplo).
Desabilite ações quando o estado não permite
.disabled(!formularioValido) evita frustração. Combine com feedback: se o botão está desabilitado, o usuário deve conseguir entender o que falta (mensagens de erro ou indicação de campos obrigatórios).
Consistência de teclado e navegação
- Use
.submitLabel(.next)e.onSubmitpara avançar. - Desabilite autocorreção em e-mail e usuário.
- Use
.textInputAutocapitalization(.never)em e-mail.
Variações úteis
Campo numérico com teclado apropriado
Se você precisar de um campo numérico (ex.: CPF, idade, código), use .keyboardType(.numberPad). Lembre que o number pad não tem botão “Done” por padrão; você pode fornecer um botão na toolbar do teclado.
@State private var codigo = ""
TextField("Código", text: $codigo)
.keyboardType(.numberPad)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("OK") { foco = nil }
}
}Validação em tempo real após a primeira tentativa
Você já tem isso com tentouEnviar. Depois que vira true, as mensagens passam a reagir conforme o usuário corrige os campos, porque as condições dependem do estado atual.