Design de software

Criando uma máquina de estados (FSM) com Swift enum sem o padrão State

Ao criar um app iOS, você inevitavelmente encontra esta situação: uma tela mistura os estados de carregamento, sucesso, erro e dados vazios.

4 min de leitura
Imagem de capa de Criando uma máquina de estados (FSM) com Swift enum sem o padrão State

Ao criar um app iOS, você inevitavelmente encontra esta situação: uma tela mistura os estados de carregamento, sucesso, erro e dados vazios.

Gerenciar várias variáveis Bool, como isLoading, hasError e isEmpty, vira um inferno conforme elas aumentam.

Um estado de carregamento com o erro também como true. Não faz sentido lógico, mas o código pode criar essa combinação facilmente.

Indo direto ao ponto: você pode organizar isso de forma muito mais limpa criando uma FSM com um único enum do Swift, sem o padrão State.

Hoje, vamos mostrar com código como sair do inferno dos Bool.


O que é uma máquina de estados (FSM)?

Não precisa pensar nisso de forma complicada.

Uma máquina de estados, FSM (Finite State Machine), é o conceito de que o número de estados possíveis é definido e as transições seguem regras estabelecidas.

Pense em um semáforo.

Verde → amarelo → vermelho. Ele muda apenas nessa ordem; nunca pula diretamente do verde para o vermelho.

As telas do app funcionam da mesma forma. Elas passam de 로딩성공 ou de 로딩실패; não deveria existir um estado que seja 성공 e 로딩 ao mesmo tempo.

Mas, ao usar várias variáveis Bool, o código pode criar estados que não deveriam existir.

É exatamente isso que o enum impede.


Como criar uma máquina de estados com Swift enum

Os enum do Swift não são apenas listas de constantes.

O ponto principal é que cada case pode armazenar um valor (associated value). Assim, um único enum pode conter estado e dados juntos.

Defini os estados da tela assim.

enum LoadState {
    case idle              // Nada feito ainda
    case loading           // Carregando
    case loaded([Item])    // Sucesso, com os dados
    case failed(Error)     // Falha, com o erro
}

Como você pode ver, loadedcarrega um array de itens, enquanto failedcarrega um erro.

Se houver sucesso, sempre haverá dados; se houver falha, sempre haverá um erro. Um estado estranho, como sucesso sem dados, simplesmente não pode ser criado.

Na View, basta observar esse único estado e renderizar a tela.

switch state {
case .idle:    EmptyView()
case .loading: ProgressView()
case .loaded(let items): ItemList(items)
case .failed(let error): ErrorView(error)
}

Como switchobriga o tratamento de todos os case, o compilador avisa imediatamente quando você adiciona um estado e deixa algum caso sem tratamento.

Essa é uma grande vantagem: o compilador identifica estados esquecidos, em vez de depender das pessoas.

O ponto principal é incluir os dados no sucesso e o erro na falha
O ponto principal é incluir os dados no sucesso e o erro na falha

Dá para ficar sem o padrão State?

Nos livros de orientação a objetos, o gerenciamento de estados normalmente é ensinado com o padrão State.

Você cria uma classe para cada estado, agrupa tudo com um protocolo e coloca a lógica de transição em cada classe.

Sendo sincero, isso costuma ser exagerado para gerenciar estados de tela.

Fiz uma comparação rápida entre as duas abordagens. (Com base na minha experiência prática em 2026)

Item enum FSM Padrão State
Número de arquivos/tipos Um enum Uma classe por estado
Prevenção de estados esquecidos Um switch obrigatório permite que o compilador identifique o problema A verificação precisa ser feita manualmente
Inclusão de dados Natural com associated value Gerenciada separadamente com propriedades
Escala adequada Cerca de 3 a 7 estados Quando a lógica de cada estado é muito complexa

Se a quantidade de estados for razoável e a lógica de cada um não for pesada, uma FSM com enum é muito mais leve e segura.

Por outro lado, se cada estado tiver dezenas de linhas de comportamento complexo, vale considerar o padrão State ou separar a lógica em objetos próprios.

Não existe uma resposta certa para todos os casos.


Como gerenciar as transições de estado?

Você pode perguntar se, ao criar apenas um enum, não seria possível mudar livremente para qualquer estado.

Boa pergunta. Por isso, concentro as regras de transição em uma única função.

mutating func fetch() {
    guard case .idle = self else { return } // idleIniciar apenas quando estiver idle
    self = .loading
}

guard casedeixa explícito: “só passe para loading quando o estado atual for idle”.

Ao manter as condições de transição em um único lugar, os estados fluem apenas por caminhos definidos, como em um semáforo.

Assim, mesmo ao revisitar o código depois, você vê de imediato de onde cada estado pode vir e para onde pode ir.

Estados que fluem apenas por caminhos definidos
Estados que fluem apenas por caminhos definidos

P. E se houver mais de cinco estados?

O enum continua funcionando bem. Porém, quando as regras de transição ficam complexas, recomendo dividir a função em funções menores por estado.

P. Ele combina bem com SwiftUI?

Combina muito. Se você mantiver o estado do enum em @State ou @Published e renderizar a View com switch, o estado e a tela avançam em sincronia.


No começo, alguns Bool podem parecer suficientes, mas quando os estados começam a se entrelaçar, você acaba voltando para uma FSM com enum.

Se o gerenciamento do estado da tela está causando problemas, experimente começar pelo enum que vimos hoje. Você ficará surpreso com a rapidez com que tudo melhora.

Continue lendo