La inyección de dependencias es un patrón bastante popular que le permite configurar de manera flexible el sistema y construir correctamente las dependencias de los componentes de este sistema entre sí. Gracias a la escritura, Swift le permite usar marcos convenientes con los que puede describir muy brevemente el gráfico de dependencia. Hoy quiero hablar un poco sobre uno de estos marcos: DITranquillity
.
Este tutorial cubrirá las siguientes características de la biblioteca:
- Registro de tipo
- Implementación de inicialización
- Incrustar en una variable
- Dependencias cíclicas de los componentes.
- Usando la biblioteca con
UIStoryboard
Descripción del componente
La aplicación constará de los siguientes componentes principales: ViewController
, Router
, Presenter
, Networking
: estos son componentes bastante comunes en cualquier aplicación de iOS.
ViewController
y Router
se introducirán entre sí cíclicamente.
Preparación
Primero, cree una aplicación de vista única en Xcode, agregue DITranquillity usando CocoaPods . Cree la jerarquía de archivos necesaria, luego agregue un segundo controlador a Main.storyboard y conéctelo usando StoryboardSegue
. Como resultado, se debe obtener la siguiente estructura de archivos:
Cree dependencias en las clases de la siguiente manera:
Declaración de componente protocol Presenter: class { func getCounter(completion: @escaping (Int) -> Void) } class MyPresenter: Presenter { private let networking: Networking init(networking: Networking) { self.networking = networking } func getCounter(completion: @escaping (Int) -> Void) {
protocol Networking: class { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) } class MyNetworking: Networking { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) {
protocol Router: class { func presentNewController() } class MyRouter: Router { unowned let viewController: ViewController init(viewController: ViewController) { self.viewController = viewController } func presentNewController() {
class ViewController: UIViewController { var presenter: Presenter! var router: Router! }
Limitaciones
A diferencia de otras clases, ViewController
es creado no por nosotros, sino por la biblioteca UIKit dentro de la implementación UIStoryboard.instantiateViewController
, por lo tanto, usando el guión gráfico, no podemos inyectar dependencias en los herederos del UIViewController
usando el inicializador. Así UIView
con los herederos de UIView
y UITableViewCell
.
Tenga en cuenta que los objetos ocultos detrás de los protocolos están integrados en todas las clases. Esta es una de las tareas principales de implementar dependencias: hacer dependencias no de implementaciones, sino de interfaces. Esto ayudará en el futuro a proporcionar diferentes implementaciones de protocolo para reutilizar o probar componentes.
Inyección de dependencia
Después de crear todos los componentes del sistema, procedemos a la conexión de objetos entre ellos. En DITranquillity, el punto de partida es DIContainer
, que agrega el registro utilizando el método container.register(...)
. Para separar las dependencias en partes, DIPart
DIFramework
y DIPart
, que deben implementarse. Por conveniencia, crearemos una sola clase de ApplicationDependency
que implementará DIFramework
y servirá como lugar de registro para todas las dependencias. La interfaz DIFramework
requiere que implemente solo un método: load(container:)
.
class ApplicationDependency: DIFramework { static func load(container: DIContainer) {
Comencemos con el registro más simple que no tiene dependencias - MyNetworking
container.register(MyNetworking.init)
Este registro utiliza la implementación a través del inicializador. A pesar de que el componente en sí no tiene dependencias, se debe proporcionar el inicializador para que la biblioteca tenga claro cómo crear el componente.
Del mismo modo, registre MyPresenter
y MyRouter
.
container.register1(MyPresenter.init) container.register1(MyRouter.init)
Nota: Tenga en cuenta que no se utiliza el register1
, sino el register1
. Desafortunadamente, esto es necesario para indicar si el objeto tiene una y solo una dependencia en el inicializador. Es decir, si hay 0 o dos o más dependencias, solo necesita usar el register
. Esta restricción es un error de Swift versión 4.0 y superior.
Es hora de registrar nuestro ViewController
. Inyecta objetos no a través del inicializador, sino directamente en la variable, por lo que la descripción del registro resultará un poco más.
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter)
La sintaxis del formulario \.presenter
es SwiftKeyPath, gracias a la cual es posible implementar dependencias de manera concisa. Dado que Router
y ViewController
dependen cíclicamente entre sí, debe indicarlo explícitamente usando cycle: true
. La biblioteca en sí misma puede resolver estas dependencias sin una indicación explícita, pero este requisito se introdujo para que la persona que lee el gráfico entendiera de inmediato que hay ciclos en la cadena de dependencia. También tenga en cuenta que NO ViewController.init
ViewController.self
, sino ViewController.self
. Esto se describió anteriormente en la sección Limitaciones .
También es necesario registrar el UIStoryboard
usando un método especial.
container.registerStoryboard(name: "Main")
Ahora hemos descrito todo el gráfico de dependencia para una pantalla. Pero todavía no hay acceso a este gráfico. Debe crear un DIContainer
que le permita acceder a los objetos que DIContainer
.
static let container: DIContainer = { let container = DIContainer()
- Inicializa el contenedor
- Agregue una descripción del gráfico.
- Verificamos que hicimos todo bien. Si se comete un error, la aplicación se bloqueará no durante la resolución de dependencias, sino inmediatamente al crear un gráfico
Luego debe hacer que el contenedor sea el punto de partida de la aplicación. Para hacer esto, implementamos el método didFinishLaunchingWithOptions
en didFinishLaunchingWithOptions
en lugar de especificar Main.storyboard
como el punto de inicio en la configuración del proyecto.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let storyboard: UIStoryboard = ApplicationDependency.container.resolve() window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true }
Lanzamiento
En el primer inicio, se producirá una caída y la validación fallará por los siguientes motivos:
- El contenedor no encontrará los tipos
Router
, Presenter
, Networking
, porque registramos solo objetos. Si queremos dar acceso no a implementaciones, sino a interfaces, debemos especificar explícitamente las interfaces - El contenedor no comprende cómo resolver la dependencia cíclica, porque es necesario indicar explícitamente qué objetos no deben recrearse cada vez que se resuelve el gráfico.
Es fácil corregir el primer error: hay un método especial que le permite especificar bajo qué protocolos está disponible el método en el contenedor.
container.register(MyNetworking.init) .as(check: Networking.self) {$0}
Al describir el registro de la siguiente manera, decimos: el objeto MyNetworking
es accesible a través del protocolo de Networking
. Esto debe hacerse para todos los objetos ocultos bajo los protocolos. {$0}
agregamos para la verificación correcta de los tipos por parte del compilador.
El segundo error es un poco más complicado. Es necesario utilizar el llamado scope
, que describe con qué frecuencia se crea el objeto y cuánto vive. Para cada registro que participe en una dependencia circular, debe especificar un scope
igual a objectGraph
. Esto dejará claro al contenedor que durante la resolución es necesario reutilizar los mismos objetos creados, en lugar de crear cada vez. Por lo tanto, resulta:
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) .lifetime(.objectGraph) container.register1(MyRouter.init) .as(check: Router.self) {$0} .lifetime(.objectGraph)
Después de reiniciar, el contenedor pasa la validación con éxito y nuestro ViewController se abre con las dependencias creadas. Puede poner un punto de interrupción en viewDidLoad
y asegurarse.
Transición entre pantallas
A continuación, cree dos clases pequeñas, SecondViewController
y SecondPresenter
, agregue SecondViewController
al guión gráfico y cree un Segue
entre ellas con el identificador "RouteToSecond"
, que le permite abrir el segundo controlador desde el primero.
Agregue dos registros más a nuestra ApplicationDependency
para cada una de las nuevas clases:
container.register(SecondViewController.self) .injection(\.secondPresenter) container.register(SecondPresenter.init)
.as
necesario especificar .as
, porque no SecondPresenter
detrás del protocolo, sino que utilizamos la implementación directamente. Luego, en el método viewDidAppear
del primer controlador, llamamos a performSegue(withIdentifier: "RouteToSecond", sender: self)
, start, se abre el segundo controlador, en el que debe fijarse la dependencia secondPresenter
. Como puede ver, el contenedor vio la creación de un segundo controlador desde UIStoryboard
y UIStoryboard
éxito las dependencias.
Conclusión
Esta biblioteca le permite trabajar convenientemente con dependencias cíclicas, guiones gráficos y utiliza completamente la inferencia de tipos automática en Swift, que proporciona una sintaxis muy corta y flexible para describir el gráfico de dependencia.
Referencias
Código de muestra completo en la biblioteca github
DITranquillity en github
Artículo en inglés