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
bodypode 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
- Declare a propriedade com
@Statee inicialize. - Use o valor no
body. - 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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
@Stateno pai e outro@Stateno 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
- Crie um objeto observável.
- Injete com
.environmentObjectem um ponto alto da árvore. - Consuma com
@EnvironmentObjectnas 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”
| Ferramenta | Quem é dono do dado? | Quando usar | Observações |
|---|---|---|---|
@State | A própria view | Estado local simples | Evite expor; passe via @Binding quando necessário |
@Binding | Outra entidade (pai/VM) | Filho precisa editar estado do pai | Não armazena; apenas referencia |
@StateObject | A view que cria | ViewModel criado dentro da view | Garante instância estável durante a vida da view |
@ObservedObject | Quem passou o objeto | ViewModel injetado | Não cria nem retém como dono |
@EnvironmentObject | Um “container” acima na árvore | Estado compartilhado em várias telas | Requer 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