A medida que crece tu app de SwiftUI, solo oír «inyección de dependencias» puede hacerte encoger los hombros.
EnvironmentObject parece suficiente, pero cuando la app crece un poco empieza a fallar por todas partes.
En este artículo explico la inyección de dependencias en SwiftUI en el orden en que la organicé: de EnvironmentObject a la DI basada en protocolos (Dependency Injection).
EnvironmentObject sirve para compartir valores entre pantallas. Si quieres servicios fáciles de sustituir, la solución es abstraerlos mediante protocolos e inyectarlos.
¿Qué es exactamente la inyección de dependencias?
Muchos artículos explican la inyección de dependencias de forma complicada, pero yo la describo así.
Un objeto recibe desde fuera las herramientas que utiliza, en lugar de crearlas por sí mismo.
Por ejemplo, si un ViewModel crea directamente un servicio de red con new, después tendrás que llamar al servidor real al hacer pruebas.
Pero si proporcionas ese servicio desde fuera, durante las pruebas puedes proporcionar un servicio falso.
Eso es todo. El concepto en sí es realmente sencillo.
Empecé con EnvironmentObject
Al aprender SwiftUI, EnvironmentObject es una de las primeras cosas que encuentras.
Basta con inyectarlo una vez en una vista padre para usarlo desde cualquier vista hija, así que al principio resultaba muy cómodo.
Como se muestra abajo, inyéctalo una vez en el nivel superior y úsalo directamente desde las vistas hijas.
class UserStore: ObservableObject {
@Published var name = ""
}
// Inyección en el nivel superior
ContentView().environmentObject(UserStore())
// Desde cualquier vista hija
@EnvironmentObject var store: UserStore
Para valores compartidos en toda la app, como el estado de inicio de sesión o el tema, no hay nada mejor.
El problema apareció al aumentar el número de pantallas. Si olvidas inyectar un EnvironmentObject, la app simplemente se cierra en tiempo de ejecución. La compilación no avisa.
Como está ligado a la jerarquía de vistas, resultaba bastante incómodo usarlo en lógica pura fuera de las vistas.
Por eso pasé a la DI por protocolos
Aquí empieza lo importante. Define los servicios como protocolos, no como tipos concretos.
Este es un ejemplo que agrupa la implementación real y la implementación falsa para pruebas bajo el mismo protocolo.
protocol WeatherService {
func fetch() async -> Int
}
struct RealWeather: WeatherService {
func fetch() async -> Int { 23 }
}
struct MockWeather: WeatherService {
func fetch() async -> Int { 999 } // Valor fijo para pruebas
}
El ViewModel no conoce RealWeather. Solo conoce el contrato llamado WeatherService.
Usa RealWeather en la app y MockWeather en las pruebas para cambiar el comportamiento sin modificar ni una línea de código.
Después de cambiar a esta estructura, escribir código de pruebas me resultó mucho más fácil.
EnvironmentObject y DI por protocolos: ¿cuándo usar cada uno?
No se trata de elegir uno: cumplen funciones distintas. He resumido en una tabla el criterio que adopté en la práctica.
| Categoría | EnvironmentObject | DI por protocolos |
|---|---|---|
| Objetivo principal | Compartir estado entre pantallas | Sustituir dependencias de la lógica |
| Acoplamiento a la jerarquía de vistas | Fuerte (ligado a las vistas) | Ninguno (independiente de las vistas) |
| Sustitución en pruebas | Difícil | Muy fácil |
| Si falta la inyección | Error en tiempo de ejecución | Se evita en compilación |
En la práctica, la combinación más sencilla fue usar EnvironmentObject para el estado global y DI por protocolos para los servicios que deben sustituirse.
Resumen
Si tu app aún es pequeña, EnvironmentObject basta. Pero si te tomas en serio las pruebas, acostúmbrate desde pronto a envolver los servicios en protocolos. Tu yo del futuro te lo agradecerá.

