Artigo · Blog
SwiftUI Preview como sensor de arquitetura
Quando uma Preview começa a exigir malabarismos pra funcionar, o problema raramente é a Preview. O problema é a arquitetura. Aprendi isso da pior forma, tentando consertar o sintoma em vez da causa.
SwiftUI Preview como sensor de arquitetura
Tem um padrão que levei tempo pra nomear. Eu chamaria de Síndrome da Preview Frágil.
Você abre o canvas no Xcode, a Preview trava, você coloca um monte de mock manual, ela carrega, você segue em frente. Isso vira rotina. Semanas depois, a View tem vinte dependências injetadas na mão, o PreviewProvider tem cem linhas, e ninguém mais usa o canvas porque "é muito trabalhoso".
O time para de usar Preview. Culpa o Xcode. Culpa o SwiftUI. Nunca culpa a arquitetura.
Fui esse time por um tempo.
O problema real não é a Preview
A Preview do SwiftUI tem um contrato simples: ela instancia sua View em isolamento, fora do ciclo de vida do app, sem acesso a contexto de navegação, sem injeção automática de dependência, sem ambiente configurado. Se sua View não consegue existir nesse isolamento, ela está acoplada demais.
Isso parece óbvio escrito assim. Na prática, não é.
Num app financeiro com vários módulos de fluxo de investimento, tínhamos Views que precisavam de um EnvironmentObject que só existia se o AppCoordinator estivesse vivo. Quando tentávamos montar a Preview, o Xcode simplesmente travava ou exibia uma tela em branco porque o @EnvironmentObject nunca chegava. Solução adotada na época: criar um AppEnvironment.mock() monstruoso que inicializava metade do app. Preview passou a funcionar. Problema "resolvido".
Não era solução. Era supressão de sintoma.
O que a dificuldade de Preview está dizendo
Quando uma View é difícil de pré-visualizar, ela está sinalizando pelo menos uma dessas coisas:
Ela conhece o ambiente demais. Se a View acessa @EnvironmentObject de um tipo concreto, ela depende de quem configura esse ambiente. Trocar pra um protocolo ou usar @Environment com uma key customizada isola o contrato.
Ela faz coisas demais. View que dispara chamada de rede, acessa UserDefaults, lê Keychain ou instancia serviço dentro do body não tem o que fazer num canvas. Isso pertence ao ViewModel ou a uma camada de use case.
O ViewModel não aceita estado externo. Um ViewModel inicializado com init() sem parâmetros e que carrega tudo internamente é impossível de pré-visualizar em estado diferente do inicial. Se você não consegue construir um ViewModel já no estado "erro" ou "carregando" pra colocar na Preview, o design de estado está errado.
Como ficou diferente na prática
Comecei a usar Preview como teste de design. Antes de considerar uma View pronta, ela precisa ter pelo menos três Previews funcionando sem mock gigante:
- Estado inicial ou vazio
- Estado carregado com dados reais típicos
- Estado de erro
Se qualquer um dos três precisar de mais de dez linhas de setup, existe acoplamento que não deveria existir.
O padrão que emergiu foi separar o estado observável da lógica de carregamento. Em vez de um ViewModel que carrega e expõe estado ao mesmo tempo, o ViewModel expõe apenas @Published var state: ViewState onde ViewState é um enum com os casos relevantes. O ViewModel recebe as dependências via init. A View recebe o ViewModel via init. Preview instancia direto:
#Preview("Erro de saldo") {
SaldoView(viewModel: SaldoViewModel(
state: .error("Serviço indisponível"),
service: MockSaldoService()
))
}
Isso funciona. Sem malabarismo. Sem setup de duzentas linhas.
A observação que contraria o senso comum
Existe uma crença de que Preview é feature de designer ou de quem usa Xcode no modo bonito. Engenheiro de verdade escreve código, não fica brincando com canvas.
Discordo completamente.
Preview é o único lugar onde você força sua View a existir sem o scaffolding do app. É um teste de isolamento que o compilador não faz por você. Se você nunca usa Preview, está perdendo feedback imediato sobre acoplamento. Você vai descobrir o problema depois, quando tentar escrever um teste de UI ou quando precisar reusar o componente num contexto diferente.
A dificuldade de escrever Preview e a dificuldade de escrever teste de unidade para uma View têm a mesma causa. Não são dois problemas. São o mesmo problema com duas manifestações.
O custo que aparece mais tarde
Num projeto com vinte e poucas telas de onboarding modularizadas, o time havia abandonado Preview completamente. Cada componente vivia acoplado ao fluxo de navegação via coordinator. Reusar qualquer componente em outro módulo exigia trazer o coordinator inteiro. Modificar um estado visual específico pra testar um edge case exigia navegar manualmente até aquela tela no simulador.
O tempo de iteração em UI foi crescendo gradualmente. Ninguém conectou ao acoplamento. Parecia custo natural de um app maduro.
Quando eventualmente refatoramos as Views pra aceitar estado por injeção e isolamos as dependências de navegação, o custo de iteração caiu bastante. Não porque Preview é mágica. Porque o isolamento que Preview exige é o mesmo que facilita mudança, reuso e teste.
Trade-offs honestos
Esse modelo exige disciplina. É mais fácil colocar um @EnvironmentObject e seguir em frente do que projetar um protocolo de serviço mockável. É mais rápido deixar o ViewModel carregar tudo internamente do que expor estado como enum injetável.
E tem um custo real: mais tipos. Mais protocolos. Mais arquivos. Em times pequenos ou projetos com prazo curto, isso pode ser overhead real, não teórico.
A decisão de aplicar esse padrão não é universal. Aplico com mais rigor em componentes que serão reutilizados ou que têm estados complexos. Em telas simples de uma vez só, aceito algum acoplamento.
O erro é nunca questionar o acoplamento porque a Preview "nunca funcionou direito mesmo".
O que eu faria diferente hoje
Estabeleceria desde o início uma convenção explícita: nenhuma View vai pra code review sem pelo menos dois #Preview funcionando. Não como burocracia. Como sinal de arquitetura.
Quando a Preview não funciona, o pull request volta. Não porque a Preview é sagrada, mas porque ela está dizendo que algo no design merece atenção antes de chegar em produção.
Preview frágil é documentação de dívida técnica que o Xcode renderiza em tempo real pra você.
A maioria fecha o canvas e segue em frente. Faz sentido no curto prazo. Até o dia em que você percebe que o canvas parou de funcionar há seis meses e ninguém notou, porque o código já não conseguia mais existir fora do app que o criou.