En el artículo anterior vimos la última letra de SOLID, DIP (el principio de inversión de dependencias). Una vez entendido el principio, surge naturalmente la siguiente pregunta: «Entonces, ¿quién ensambla estas dependencias en una aplicación real y cómo lo hace?»
En proyectos pequeños basta con la DI manual (Dependency Injection, inyección de dependencias) mediante inicializadores. Pero cuando las pantallas se cuentan por decenas y el grafo de dependencias se hace más profundo, el propio código de ensamblaje empieza a ser una carga. Hoy presentamos Factory, una biblioteca DI de Swift adecuada para ese punto. El artículo usa como referencia la versión más reciente a julio de 2026: 3.3.1.
¿Hasta dónde puede aguantar la DI manual?
El código que respeta DIP tenía más o menos este aspecto.
protocol NetworkProviding {
func fetch(_ url: URL) async throws -> Data
}
final class OrderViewModel {
private let network: NetworkProviding
init(network: NetworkProviding) {
self.network = network
}
}
El código depende de protocolos y recibe las implementaciones mediante inyección externa. Hasta aquí no hace falta ninguna biblioteca. El problema está en la capa de ensamblaje.
// un punto de ensamblaje en algún lugar(Composition Root)
let network = NetworkProvider()
let repository = OrderRepository(network: network)
let analytics = AnalyticsService(network: network)
let viewModel = OrderViewModel(repository: repository, analytics: analytics)
Cuando las dependencias alcanzan tres o cuatro niveles, este código de inicialización se repite en cada pantalla. Si se añade una dependencia a una capa intermedia, hay que modificar todo el código de inicialización que pasa por ella. Es el momento en que aumenta la tentación de escapar hacia un singleton, pero ya sabes cómo los singletons dificultan las pruebas. Un contenedor DI se encarga precisamente de este problema de ensamblaje.
Qué diferencia a Factory: seguridad en tiempo de compilación
Durante mucho tiempo, Swinject se utilizó como el estándar entre las bibliotecas DI de Swift. Registra por cadena o tipo y recupera con resolve(), así que si falta un registro la compilación termina correctamente y nil aparece en tiempo de ejecución. Solo descubres el error al ejecutar la aplicación.
Factory invierte este enfoque. Como las dependencias se definen como propiedades calculadas de Container, una referencia a una dependencia inexistente simplemente no compila. Los errores tipográficos y los registros ausentes se detectan durante la compilación.
Además, es una biblioteca ligera con menos de 1.000 líneas de código ejecutable y funciona con Swift puro, sin scripts de generación de código en tiempo de compilación. No necesitas insertar herramientas en el pipeline de compilación como ocurre con Needle.
Factory en 3 minutos: uso básico
Se instala mediante SPM (Swift Package Manager). La URL del paquete es https://github.com/hmlongco/Factory y, desde 3.x, se importa con FactoryKit.
Las dependencias se registran añadiendo propiedades calculadas a una extensión de Container.
import FactoryKit
extension Container {
var networkService: Factory<NetworkProviding> {
self { NetworkProvider() }
}
var orderRepository: Factory<OrderRepositoryType> {
self { OrderRepository(network: self.networkService()) }
}
}
Hay tres formas de recuperarlas.
// 1. Inyección mediante property wrapper
final class OrderViewModel {
@Injected(\.orderRepository) private var repository
}
// 2. Llamada directa
let repository = Container.shared.orderRepository()
// 3. Inyección por inicializador — dejar el ensamblaje en manos del contenedor
extension Container {
var orderViewModel: Factory<OrderViewModel> {
self { OrderViewModel(repository: self.orderRepository()) }
}
}
La tercera opción merece atención. La clase conserva una forma pura de inyección por inicializador y no conoce en absoluto la biblioteca, mientras que el contenedor se ocupa solo del ensamblaje. Encaja exactamente con el principio tratado en el artículo sobre DIP: «ensamblar las implementaciones es responsabilidad del exterior».
En SwiftUI puedes recibir directamente un view model Observable mediante @InjectedObservable.
struct OrderView: View {
@InjectedObservable(\.orderViewModel) var viewModel
}
Ámbitos: declara la vida útil de las instancias
Otra razón para usar un contenedor DI es gestionar la vida útil de las instancias. En Factory basta con añadir un modificador al registro.
extension Container {
var networkService: Factory<NetworkProviding> {
self { NetworkProvider() }.singleton
}
var imageCache: Factory<ImageCaching> {
self { ImageCache() }.cached
}
}
- unique — Valor predeterminado. Crea una instancia nueva en cada solicitud.
- singleton — Comparte una instancia en toda la aplicación.
- cached — Devuelve la misma instancia hasta que se reinicia la caché.
- shared — Se mantiene mientras alguien conserve una referencia fuerte y se libera cuando nadie la usa.
La diferencia respecto a crear directamente un objeto singleton global es que la vida útil se declara en un único registro, en lugar de quedar dispersa por el código como static let. Si más adelante quieres cambiar singleton por cached, solo tienes que cambiar un modificador.
Donde las pruebas y los previews demuestran su valor
La razón más práctica para introducir DI son, en última instancia, las pruebas. Factory permite sobrescribir un registro directamente.
import FactoryTesting
@Suite(.container) // Aislar el contenedor en cada prueba
struct OrderViewModelTests {
@Test func loadsOrders() async {
Container.shared.orderRepository { MockOrderRepository() }
let viewModel = Container.shared.orderViewModel()
await viewModel.load()
#expect(viewModel.orders.count == 3)
}
}
En Swift Testing, añadir el trait .container al target FactoryTesting evita que el estado del contenedor se mezcle entre pruebas. En los previews de SwiftUI también puedes insertar mocks de la misma forma.
#Preview {
Container.shared.orderRepository { MockOrderRepository() }
return OrderView()
}
También existe un modificador de contexto que sustituye automáticamente la implementación solo en un entorno de ejecución concreto. Si quieres usar analíticas stub únicamente en builds de depuración, sería así.
container.analytics.onDebug { StubAnalyticsEngine() }
Con el mismo enfoque puedes declarar en el registro overrides para pruebas, previews y entornos de simulador.
Qué ha cambiado en Factory 3.x
Para quienes llegan desde 2.x, resumimos los cambios principales.
- Cambio del nombre de importación —
import Factorypasó a llamarseimport FactoryKit. La mayor parte de la migración consiste en esta sustitución. - Compatibilidad completa con Swift 6 Strict Concurrency — Al registrar un view model
@MainActor, en 2.x había que repetir@MainActor indentro del closure, pero en 3.x basta con la anotación en la declaración de la factory. - Solo SPM — Se ha suspendido la compatibilidad con CocoaPods. Los proyectos CocoaPods deben quedarse en Factory 2.5.3 o integrar el código fuente directamente.
- Compatibilidad con Swift Testing — Se ha incorporado oficialmente el aislamiento de pruebas, incluido el trait
.containermostrado arriba.
Resumen
Si DIP indica que debemos depender de abstracciones y no de concreciones, Factory reduce el coste de mantener esa dirección en un código real. La seguridad en tiempo de compilación convierte los registros ausentes en errores de build, y los ámbitos y el reemplazo de mocks se resuelven con unas pocas declaraciones.
No hace falta rehacer un proyecto que ya funciona bien con Swinject, pero en uno nuevo puedes elegir Factory como opción predeterminada. Es ligero y fácil de adoptar; si no te convence, puedes retirar solo el contenedor y conservar la estructura de inyección por inicializador.
Si la DI manual empieza a frenarte porque el código de ensamblaje se ha convertido en un lastre, pruébalo.

