iOS com SwiftUI: componentes essenciais e controles de interação

Capítulo 3

Tempo estimado de leitura: 9 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 .onSubmit para 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 .onSubmit para 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.

Agora responda o exercício sobre o conteúdo:

Ao implementar um formulário de cadastro em SwiftUI com validação e controle de foco, qual abordagem melhora a experiência ao evitar exibir erros cedo demais, mas ainda fornecer feedback enquanto o usuário corrige os campos?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Uma estratégia prática é mostrar erros apenas depois da primeira tentativa de envio (ex.: usando um estado como tentouEnviar) e então reagir em tempo real enquanto o usuário corrige. Isso reduz poluição visual e mantém feedback útil.

Próximo capitúlo

Gerenciamento de estado no SwiftUI para iOS: @State, @Binding e ciclo de vida

Arrow Right Icon
Capa do Ebook gratuito iOS para Iniciantes com SwiftUI: do zero ao primeiro app na App Store
21%

iOS para Iniciantes com SwiftUI: do zero ao primeiro app na App Store

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.