À medida que seu app SwiftUI cresce, só de ouvir “injeção de dependência” é fácil ficar tenso.
EnvironmentObject parece resolver tudo, mas, quando o app cresce um pouco, começa a ranger por todos os lados.
Neste artigo, explico a injeção de dependência no SwiftUI na ordem em que a organizei: de EnvironmentObject à DI baseada em protocolos (Dependency Injection).
EnvironmentObject é uma ferramenta para compartilhar valores entre telas. Se você quer serviços fáceis de trocar, a resposta é abstraí-los com protocolos e injetá-los.
Afinal, o que é injeção de dependência?
Há muitos textos que explicam injeção de dependência de forma complicada, mas eu a descrevo assim.
Um objeto recebe de fora as ferramentas que usa, em vez de criá-las por conta própria.
Por exemplo, se o ViewModel instancia diretamente um serviço de rede com new, depois você terá de chamar o servidor real nos testes.
Mas, se você fornecer esse serviço de fora, poderá usar um serviço falso nos testes.
É só isso. O conceito em si é realmente simples.
Comecei com EnvironmentObject
Ao aprender SwiftUI, EnvironmentObject é uma das primeiras coisas que você encontra.
Basta injetá-lo uma vez em uma view pai para acessá-lo de qualquer view filha, então no começo foi muito conveniente.
Como no exemplo abaixo, injete-o uma vez no nível superior e acesse-o diretamente nas views filhas.
class UserStore: ObservableObject {
@Published var name = ""
}
// Injetado no nível superior
ContentView().environmentObject(UserStore())
// De qualquer view filha
@EnvironmentObject var store: UserStore
Para valores compartilhados em todo o app, como estado de login ou tema, não há opção melhor.
O problema apareceu quando o número de telas aumentou. Se você esquecer de injetar um EnvironmentObject, o app simplesmente cai em runtime. A compilação não avisa.
Como ele está preso à hierarquia de views, era estranho usá-lo em lógica pura fora das views.
Por isso migrei para DI com protocolos
É aqui que a coisa fica séria. Defina os serviços como protocolos, não como tipos concretos.
Este é um exemplo que reúne a implementação real e a implementação falsa para testes no mesmo protocolo.
protocol WeatherService {
func fetch() async -> Int
}
struct RealWeather: WeatherService {
func fetch() async -> Int { 23 }
}
struct MockWeather: WeatherService {
func fetch() async -> Int { 999 } // Valor fixo para testes
}
O ViewModel não conhece RealWeather. Ele conhece apenas o contrato chamado WeatherService.
Use RealWeather no app e MockWeather nos testes para mudar o comportamento sem alterar uma única linha de código.
Depois de mudar para essa estrutura, escrever testes ficou muito mais fácil para mim.
EnvironmentObject e DI com protocolos: quando usar cada um?
Não é preciso escolher apenas um: eles têm papéis diferentes. Resumi em uma tabela o critério que adotei na prática.
| Categoria | EnvironmentObject | DI com protocolos |
|---|---|---|
| Objetivo principal | Compartilhar estado entre telas | Trocar dependências da lógica |
| Acoplamento à hierarquia de views | Forte (preso às views) | Nenhum (independente das views) |
| Troca nos testes | Difícil | Muito fácil |
| Quando falta a injeção | Crash em runtime | Evitado em tempo de compilação |
Na prática, a combinação mais tranquila foi usar EnvironmentObject para estado global e DI com protocolos para serviços que precisam ser trocados.
Resumo
Se o app ainda é pequeno, EnvironmentObject basta. Mas, se você leva os testes a sério, crie desde cedo o hábito de envolver os serviços em protocolos. Seu eu do futuro agradece.

