Design de software

DI no SwiftUI: EnvironmentObject e protocolos

À medida que seu app SwiftUI cresce, só de ouvir “injeção de dependência” é fácil ficar tenso.

3 min de leitura
Imagem de capa de DI no SwiftUI: EnvironmentObject e protocolos

À 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.

Se você esquecer uma injeção, a compilação fica em silêncio e o app cai em runtime.
Se você esquecer uma injeção, a compilação fica em silêncio e o app cai em runtime.

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.

O ViewModel só enxerga o protocolo
O ViewModel só enxerga o protocolo

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.

Na prática, basta trocar RealWeather por MockWeather
Na prática, basta trocar RealWeather por MockWeather

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.