⬆️ ⬇️

Modular development or the way there, not back



How we came to a new approach to working with modules in the RaiffeisenBank iOS application.



Problem



In Raiffeisenbank's applications, each screen consists of several modules that are as independent of each other as possible. "Module" we call the visual component, which has its own presentation. When designing an application, it is very important to write logic so that the modules are independent and can be easily added or removed without refactoring.



What difficulties we faced:



Highlighting abstraction over architectural patterns

Already at the first stage of development, it became clear that we did not want to be tied to a specific architectural pattern. MVC is good if you want to display a page with some information. At the same time, user interaction is minimal or not at all. For example: the page “about the company” or “user agreement”. VIPER is a good tool for complex modules that have their own logic for working with services, routing and a whole bunch.



Problem of interaction and encapsulation

Each architectural pattern has its own structure of construction and its own protocols, which impose restrictions on working with the module. To abstract a module, you need to highlight the main input / output interfaces.

')

Routing logic selection

A module as a visual unit should not and cannot be aware of where and how it is shown. The same module must and can be implemented as an independent unit on any screen or as a composition. Responsibility for this cannot be placed on the module itself.



Previous solution: // Bad deal



We wrote the first solution in Objective-C, and it was based on NSProxy. The problem of encapsulating the architectural pattern was solved by defenition, which was determined by the specified conditions, that is, the input / output module, which allowed to proxy any calls to the module to its input and receive messages, if any, via output .



It was a step forward, but new difficulties arose:





In addition to NSProxy, we also implemented the routing by looking at the idea of ​​ViperMcFlurry: we made a category on the ViewController , which began to grow as different variants of displaying the module on the screen appeared. Of course, we divided the category, but it was still far from a good solution.



In general ... the first pancake is lumpy, it became clear that the problem should be solved differently.



Solution: // Final



Realizing that with NSProxy further no way, they took markers in their hands and went to draw. As a result, the RFModule protocol was identified :



@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } } 


We purposely abandoned the associated types at the protocol level, and there was a good reason for this: at that time 90% of the code was in Objective-C. The interaction between ObjC ← → Swift modules would not be possible.



In order to use generics and ensure the use of modules, we introduced the Module class, which satisfies the protocol.

RFModule :



 final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)? //... public var input: I? { get { return inputObjc as? I} set { inputObjc = newValue as AnyObject } } public var output: O? { get { return outputObjc as? O} set { outputObjc = newValue as AnyObject } } @objc(input) public weak var inputObjc: AnyObject? @objc(moduleOutput) public weak var outputObjc: AnyObject? { didSet{ setOutput?(output) } } } @objc protocol RFModule { var view: ViewController { get } @objc(input) var inputObjc: AnyObject? { get } @objc(moduleOutput) var outputObjc: AnyObject? { get set } var transition: Transitioning { get set } } public extension RFModule { public var input: AnyObject? { return inputObjc } public var output: AnyObject? { get { return outputObjc } set { outputObjc = newValue} } } 


So we got a typed module. And as a matter of fact in Swift class Module is used , and in Objective-C RFModule . In addition, it turned out to be a handy tool when mashing types in the place where you need to create arrays: for example, TabContainer .



Since the DI of creating a module is in UserStory's scopes , and assigning the value of output to the place where it will be used cannot describe a simple setter. "SetOutput" is, in fact, a defenition which, at the stage of assigning output, will transfer it to the person responsible, depending on the logic of the module.



 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 


Transitioning is a protocol whose implementations, as the name implies, are responsible for the logic of showing and hiding a module.



 protocol Transitioning { var destination: ViewController? { get } // should be weak func perform(_ completion: (()->())?) // present func reverse(_ completion: (()->())?) // dissmiss } 


For display it is called - perform , for concealment - reverse . Despite the fact that the protocol has a destination and at first it seems that there should be a source . In fact, the source may not be, and its type is not always a ViewController . For example, if we need the module to open in a new window - this is a Window , and if you need embed , you need 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) } } 


Thus, we got rid of the idea to write extensions on the ViewController and described the logic of how we show our modules in various objects. This gave us flexibility in routing, i.e. Now we can show any module both independently and in a complex, and also vary between how it all appears on the screen: in the window (Window), Present, in navigation (push to navigation), embed, in the curtain (cover) .



It's all?



There is one more thing that does not give rest. For the opportunity to easily choose the way the module is displayed and the removal of this logic from it, we paid by losing the ability to set the appearance properties. For example, if we show it in Navigation, we need to specify what color barTintColor should be; or, if we show the module in the blind, there is a need to set the color of the handler .



So far, we have solved this problem with the not typed appearance: Any property, and Transitioning, when opening a module, leads to the type with which it works, and, if it did it, it takes the necessary properties.

Source: https://habr.com/ru/post/448206/



All Articles