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:
- The proxy interface did not guarantee the implementation of the input protocol;
- Output had to be described, even if it was not needed;
- It was necessary to add the output property to the input interface.
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)?
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 }
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.