Cómo llegamos a un nuevo enfoque para trabajar con módulos en la aplicación iOS RaiffeisenBank.El problema
En las aplicaciones de Raiffeisenbank, cada pantalla consta de varios módulos que son lo más independientes posible entre sí. "Módulo" lo llamamos un componente visual que tiene su propia idea. Al diseñar una aplicación, es muy importante escribir la lógica para que los módulos sean independientes y puedan agregarse o eliminarse fácilmente sin tener que recurrir a la refactorización.
Qué dificultades enfrentamos:
Destacando la abstracción sobre los patrones arquitectónicosYa en la primera etapa de desarrollo, quedó claro que no queríamos estar atados a un patrón arquitectónico específico. MVC es bueno si necesita mostrar una página con alguna información. Al mismo tiempo, la interacción con el usuario es mínima o nada. Por ejemplo: la página "sobre la empresa" o "acuerdo de usuario". VIPER es una buena herramienta para módulos complejos que tienen su propia lógica de trabajo con servicios, enrutamiento y mucho de todo.
El problema de la interacción y la encapsulación.Cada patrón arquitectónico tiene su propia estructura de construcción y sus propios protocolos, que imponen restricciones para trabajar con el módulo. Para abstraer el módulo, debe resaltar las principales interfaces de interacción de
entrada / salida .
Destacando la lógica de enrutamientoUn módulo como unidad visual no debe ni puede saber dónde y cómo se muestra. Uno y el mismo módulo debe y puede implementarse como una unidad independiente en cualquier pantalla o como una composición. La responsabilidad de esto no se puede culpar al módulo en sí.
Solución anterior: // Mal negocio
La primera solución que escribimos en Objective-C, y se basó en NSProxy. El problema de la encapsulación del patrón arquitectónico se resolvió mediante la defensa, que se determinó por las condiciones dadas, es decir, la
entrada / salida del módulo, lo que hizo posible que las llamadas al módulo sean proxy a su
entrada y recibir mensajes a través de la
salida , si corresponde.
Fue un paso adelante, pero surgieron nuevas dificultades:
- La interfaz proxy no garantizaba la implementación del protocolo de entrada ;
- La salida tenía que ser descrita, incluso si no era necesaria;
- Era necesario agregar la propiedad de salida a la interfaz de entrada .
Además de
NSProxy, también implementamos el enrutamiento al observar la idea de ViperMcFlurry: creamos una categoría en
ViewController , que comenzó a crecer a medida que aparecían diferentes opciones para mostrar el módulo en la pantalla. Por supuesto, dividimos la categoría, pero aún estaba lejos de ser una buena solución.
En general ... el primer panqueque está lleno de bultos, quedó claro que debe resolver el problema de manera diferente.
Solución: // Final
Al
darnos cuenta de que no había
nada más con
NSProxy , tomamos marcadores y fuimos a dibujar. Como resultado,
aislamos el protocolo
RFModule :
@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } }
A propósito abandonamos los tipos asociados a nivel de protocolo, y había una buena razón para esto: en ese momento, el 90% del código estaba en Objective-C. Interoperabilidad entre módulos ObjC ← → Swift no sería posible.
Para poder seguir utilizando genéricos y garantizar el uso de módulos de tipo, introdujimos la clase
Módulo que satisface el protocolo
RFModule :
final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)?
Entonces tenemos un módulo escrito. Y de hecho, Swift usa la clase
Módulo , y en el Objective-C
RFModule . Además, resultó ser una herramienta conveniente para combinar tipos en el lugar donde necesita crear matrices: por ejemplo,
TabContainer .
Dado que la DI para crear el módulo está en el
ámbito de UserStory, y asignar el valor de salida en el lugar donde se utilizará no puede describir un simple seter.
"SetOutput" es, en esencia, una
defensa que, en la etapa de asignación de
salida, la pasará a la persona responsable, dependiendo de la lógica del módulo.
class SomeViewController: UIViewController, ModuleInput { weak var delegate: ModuleOutput } class Assembly { func someModule() -> Module<ModuleInput, ModuleOutput> { let view = SomeViewController() let module = Module<ModuleInput, ModuleOutput>(view: view, input: view) { [weak view] output in view?.delegate = output } return module } } ... let assembly: Assembly let module = assembly.someModule() module.output = self
La transición es un protocolo cuyas implementaciones, como su nombre lo indica, son responsables de la lógica de mostrar y ocultar el módulo.
protocol Transitioning { var destination: ViewController? { get }
Para la visualización es causada -
realizar , para ocultar -
invertir . A pesar del hecho de que hay un
destino en el protocolo y al principio parece que debería haber una
fuente . De hecho, la
fuente puede no ser, y su tipo no siempre es
ViewController . Por ejemplo, si necesitamos que el módulo se abra en una nueva ventana, esta es
Window , y si necesitamos
incrustar , necesitamos AND parent:
ViewController AND container:
UIView .
class PresentTransition: Transitioning { weak var source: ViewController? weak var destination: ViewController? ... func perform(_ completion: (()->())?) { source.present(viewController: self.destinaton) } }
Por lo tanto, nos deshicimos de la idea de escribir extensiones en el
ViewController y describimos la lógica de cómo mostramos nuestros módulos en varios objetos. Esto nos dio flexibilidad en el enrutamiento, es decir ahora podemos mostrar cualquier módulo tanto de forma independiente como en un complejo, así como variar entre cómo se muestra todo en la pantalla: en la ventana (Ventana), Presente, en la navegación (presionar para navegar), incrustar, en la cortina (cubierta) .
¿Eso es todo?
Hay una cosa más que es inquietante hasta ahora. Para tener la oportunidad de elegir fácilmente la forma en que se muestra el módulo y eliminar esta lógica, pagamos la pérdida de la capacidad de establecer las propiedades de apariencia. Por ejemplo, si lo mostramos en Navigation, necesitamos especificar qué color debe ser
barTintColor ; o, si mostramos el módulo en la cortina, es necesario establecer el color del
controlador .
Hasta ahora, hemos resuelto este problema con la apariencia sin tipo: cualquier propiedad, y la transición al abrir el módulo conduce al tipo con el que funciona, y si tiene éxito, le quita las propiedades necesarias.