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

Capítulo 4

Tempo estimado de leitura: 8 minutos

+ Exercício

Como o SwiftUI reage a mudanças de estado

No SwiftUI, a interface é uma função do estado: quando algum dado observado muda, o framework invalida a view e recalcula o body para produzir uma nova descrição da UI. Você não “manda atualizar” a tela manualmente; você muda o estado, e o SwiftUI decide o que precisa redesenhar.

O que significa “recalcular o body”

  • Recalcular não é o mesmo que “recriar tudo na tela”. O SwiftUI compara a nova árvore de views com a anterior e aplica apenas as diferenças necessárias.
  • O body pode ser chamado muitas vezes. Por isso, evite colocar nele trabalho pesado (ex.: parsing grande, chamadas de rede, loops custosos). Prefira calcular valores simples ou delegar para um ViewModel.
  • Quando um estado muda, o SwiftUI invalida a parte da hierarquia que depende daquele estado. Em geral, views ancestrais podem ser recalculadas, mas apenas subárvores afetadas serão atualizadas visualmente.

Um exemplo mínimo de invalidação

struct ContadorView: View {    @State private var count = 0    var body: some View {        VStack(spacing: 12) {            Text("Contagem: \(count)")            Button("Incrementar") {                count += 1            }        }    }}

A cada toque no botão, count muda, o SwiftUI recalcula o body e o texto passa a refletir o novo valor.

@State: estado local e “source of truth” dentro da view

@State é usado para armazenar estado privado e local de uma view. Ele deve ser a “fonte da verdade” (source of truth) quando o dado pertence àquela view e não precisa ser controlado por outra parte do app.

Quando usar @State

  • Valores simples: Bool, Int, String, enums.
  • Controle de UI local: mostrar/ocultar um sheet, alternar um toggle, texto de um campo, seleção local.
  • Quando a view é dona do dado e não precisa compartilhá-lo como fonte principal.

Passo a passo: criando estado local

  1. Declare a propriedade com @State e inicialize.
  2. Use o valor no body.
  3. Altere o valor em uma ação (ex.: botão).
struct FiltroView: View {    @State private var mostrarSomenteFavoritos = false    var body: some View {        VStack {            Toggle("Somente favoritos", isOn: $mostrarSomenteFavoritos)            Text(mostrarSomenteFavoritos ? "Filtrando favoritos" : "Mostrando todos")        }        .padding()    }}

Note o $ antes de mostrarSomenteFavoritos: isso cria um Binding para o Toggle (o controle precisa ler e escrever o valor).

@Binding: passando controle de um estado para uma view filha

@Binding é uma referência “editável” para um valor que pertence a outra view (ou a outro dono do estado). A view filha não é dona do dado; ela apenas lê e escreve no estado do pai.

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

Quando usar @Binding

  • Componentes reutilizáveis que precisam editar um valor externo.
  • Quando o estado deve continuar sendo a fonte da verdade no pai (ou em um ViewModel), mas o filho precisa alterar.
  • Para evitar duplicação de estado (um erro comum é ter @State no pai e outro @State no filho para o mesmo dado).

Exemplo: componente filho editando estado do pai

struct CampoNome: View {    @Binding var nome: String    var body: some View {        TextField("Nome", text: $nome)            .textFieldStyle(.roundedBorder)    }}struct CadastroView: View {    @State private var nome = ""    var body: some View {        VStack(spacing: 12) {            CampoNome(nome: $nome)            Text("Olá, \(nome)")        }        .padding()    }}

Regra prática: se o filho precisa editar algo que o pai controla, use @Binding.

Fluxo completo: tela pai controla dados e passa bindings para filhos

Vamos montar um fluxo simples de “Lista de tarefas” onde a tela pai controla a lista e passa bindings para componentes filhos (linha editável e formulário).

Modelo simples

struct Tarefa: Identifiable, Equatable {    let id: UUID = UUID()    var titulo: String    var concluida: Bool = false}

Filho 1: linha reutilizável que edita uma tarefa via Binding

struct LinhaTarefaView: View {    @Binding var tarefa: Tarefa    var body: some View {        HStack {            Button {                tarefa.concluida.toggle()            } label: {                Image(systemName: tarefa.concluida ? "checkmark.circle.fill" : "circle")            }            TextField("Título", text: $tarefa.titulo)        }    }}

A linha não cria estado próprio para titulo ou concluida. Ela edita diretamente a tarefa que o pai fornece.

Filho 2: formulário para adicionar uma nova tarefa

struct NovaTarefaView: View {    @Binding var texto: String    let onAdicionar: () -> Void    var body: some View {        HStack {            TextField("Nova tarefa", text: $texto)                .textFieldStyle(.roundedBorder)            Button("Adicionar") {                onAdicionar()            }            .disabled(texto.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)        }    }}

O texto do campo é controlado pelo pai via @Binding, e a ação de adicionar é enviada por closure.

Pai: controla a lista e distribui bindings

struct ListaTarefasView: View {    @State private var tarefas: [Tarefa] = [        Tarefa(titulo: "Estudar SwiftUI"),        Tarefa(titulo: "Criar primeira tela")    ]    @State private var textoNovaTarefa = ""    var body: some View {        VStack(spacing: 16) {            NovaTarefaView(texto: $textoNovaTarefa) {                let titulo = textoNovaTarefa.trimmingCharacters(in: .whitespacesAndNewlines)                tarefas.append(Tarefa(titulo: titulo))                textoNovaTarefa = ""            }            List {                ForEach($tarefas) { $tarefa in                    LinhaTarefaView(tarefa: $tarefa)                }                .onDelete { indexSet in                    tarefas.remove(atOffsets: indexSet)                }            }        }        .padding()    }}

O ponto-chave aqui é ForEach($tarefas): ele produz bindings para cada elemento, permitindo que a linha edite a tarefa sem criar estados duplicados.

@ObservedObject, @StateObject e ViewModel: separando lógica da UI

Quando o estado deixa de ser “apenas local” e passa a envolver regras de negócio, validações, carregamento de dados ou compartilhamento entre telas, é comum mover esse estado para um objeto separado (ViewModel) que a view observa.

Para isso, usamos um tipo que conforma com ObservableObject e marca propriedades com @Published. Quando uma propriedade @Published muda, o SwiftUI invalida as views que observam esse objeto.

ViewModel simples com ObservableObject

final class TarefasViewModel: ObservableObject {    @Published var tarefas: [Tarefa] = []    @Published var textoNovaTarefa: String = ""    func adicionar() {        let titulo = textoNovaTarefa.trimmingCharacters(in: .whitespacesAndNewlines)        guard !titulo.isEmpty else { return }        tarefas.append(Tarefa(titulo: titulo))        textoNovaTarefa = ""    }    func remover(at offsets: IndexSet) {        tarefas.remove(atOffsets: offsets)    }}

@StateObject: quando a view CRIA e É DONA do ViewModel

@StateObject deve ser usado quando a view instancia o ViewModel e precisa que ele tenha ciclo de vida estável enquanto a view existir. Isso evita que o objeto seja recriado em recomputações do body.

struct ListaTarefasComVMView: View {    @StateObject private var vm = TarefasViewModel()    var body: some View {        VStack(spacing: 16) {            NovaTarefaView(texto: $vm.textoNovaTarefa) {                vm.adicionar()            }            List {                ForEach($vm.tarefas) { $tarefa in                    LinhaTarefaView(tarefa: $tarefa)                }                .onDelete(perform: vm.remover)            }        }        .padding()    }}

@ObservedObject: quando a view RECEBE um ViewModel de fora

@ObservedObject é usado quando o objeto é criado em outro lugar (por exemplo, em uma view pai) e injetado na view atual. A view observa mudanças, mas não é a dona do ciclo de vida.

struct TelaPai: View {    @StateObject private var vm = TarefasViewModel()    var body: some View {        TelaFilha(vm: vm)    }}struct TelaFilha: View {    @ObservedObject var vm: TarefasViewModel    var body: some View {        Text("Total: \(vm.tarefas.count)")    }}

Regra prática: quem cria usa @StateObject; quem recebe usa @ObservedObject.

@EnvironmentObject: compartilhando estado global (sem passar parâmetro por parâmetro)

@EnvironmentObject é útil quando várias telas distantes na hierarquia precisam do mesmo estado (ex.: sessão do usuário, carrinho, configurações). Em vez de passar o objeto por vários inicializadores, você injeta uma vez no topo e lê onde precisar.

Como configurar

  1. Crie um objeto observável.
  2. Injete com .environmentObject em um ponto alto da árvore.
  3. Consuma com @EnvironmentObject nas views descendentes.
final class SessaoUsuario: ObservableObject {    @Published var nome: String = ""    @Published var logado: Bool = false}
struct AppRootView: View {    @StateObject private var sessao = SessaoUsuario()    var body: some View {        NavegacaoView()            .environmentObject(sessao)    }}
struct PerfilView: View {    @EnvironmentObject var sessao: SessaoUsuario    var body: some View {        VStack {            TextField("Nome", text: $sessao.nome)            Toggle("Logado", isOn: $sessao.logado)        }        .padding()    }}

Atenção: se você acessar um @EnvironmentObject sem tê-lo injetado, o app vai falhar em runtime. Em projetos maiores, injete no ponto mais alto possível (por exemplo, na raiz do fluxo).

Ciclo de vida do estado: onde cada um “vive”

FerramentaQuem é dono do dado?Quando usarObservações
@StateA própria viewEstado local simplesEvite expor; passe via @Binding quando necessário
@BindingOutra entidade (pai/VM)Filho precisa editar estado do paiNão armazena; apenas referencia
@StateObjectA view que criaViewModel criado dentro da viewGarante instância estável durante a vida da view
@ObservedObjectQuem passou o objetoViewModel injetadoNão cria nem retém como dono
@EnvironmentObjectUm “container” acima na árvoreEstado compartilhado em várias telasRequer injeção com .environmentObject

Padrões de organização para iniciantes (sem estados duplicados)

1) Defina uma única fonte da verdade

Escolha onde o dado “mora” e evite manter cópias sincronizadas. Exemplo de problema comum: ter @State var nome no pai e outro @State var nome no filho, tentando manter ambos iguais. Isso cria inconsistência.

Preferência: o pai mantém @State e o filho recebe @Binding, ou o ViewModel mantém @Published e as views leem/alteram via bindings.

2) Use ViewModel para regras e ações, não para layout

  • Coloque no ViewModel: lista de dados, validações, ações (adicionar/remover), estados de carregamento.
  • Deixe na view: composição de UI, navegação e pequenas transformações visuais.

3) Prefira ações explícitas em vez de “mágica” no body

Evite fazer mutações de estado em computações do body. Em vez disso, exponha métodos no ViewModel e chame em ações (botões) ou em eventos apropriados.

4) Passe dados para baixo e eventos para cima

  • Dados para baixo: via propriedades normais, @Binding, ou objetos observáveis.
  • Eventos para cima: via closures (ex.: onAdicionar, onRemover), ou métodos do ViewModel.

5) Checklist rápido: qual property wrapper escolher?

  • “Esse valor é só desta tela?” → @State
  • “Essa subview precisa editar um valor do pai?” → @Binding
  • “Preciso de um objeto com lógica e estado observado, criado aqui?” → @StateObject
  • “Recebi um objeto observado de outra view?” → @ObservedObject
  • “Várias telas distantes precisam do mesmo objeto?” → @EnvironmentObject

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

Em SwiftUI, quando uma subview precisa editar um valor cujo “source of truth” está no pai, qual abordagem é a mais adequada para evitar duplicação de estado?

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

Você errou! Tente novamente.

Quando o pai é a fonte da verdade, a subview deve receber um @Binding para ler e escrever no mesmo estado, evitando estados duplicados e inconsistência.

Próximo capitúlo

Navegação em apps iOS com SwiftUI: NavigationStack, rotas e telas

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

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.