Comment nous sommes arrivés à une nouvelle approche pour travailler avec des modules dans l'application iOS RaiffeisenBank.Le problème
Dans les applications Raiffeisenbank, chaque écran est composé de plusieurs modules aussi indépendants que possible les uns des autres. «Module», nous appelons un composant visuel qui a sa propre idée. Lors de la conception d'une application, il est très important d'écrire une logique afin que les modules soient indépendants et qu'ils puissent être facilement ajoutés ou supprimés sans recourir à une refactorisation.
Quelles difficultés nous avons rencontrées:
Mettre en évidence l'abstraction sur les modèles architecturauxDéjà au premier stade de développement, il est devenu clair que nous ne voulions pas être liés à un modèle architectural spécifique. MVC est bon si vous devez afficher une page contenant des informations. Dans le même temps, l'interaction avec l'utilisateur est minime ou pas du tout. Par exemple: la page «à propos de l'entreprise» ou «accord d'utilisation». VIPER est un bon outil pour les modules complexes qui ont leur propre logique de travail avec les services, le routage et beaucoup de tout.
Le problème de l'interaction et de l'encapsulationChaque modèle architectural a sa propre structure de construction et ses propres protocoles, qui imposent des restrictions sur l'utilisation du module. Pour résumer le module, vous devez mettre en évidence les principales interfaces d'interaction
entrée / sortie .
Mise en évidence de la logique de routageUn module en tant qu'unité visuelle ne doit pas et ne peut pas savoir où et comment il est affiché. Un même module doit et peut être implémenté en tant qu'unité indépendante sur n'importe quel écran ou en tant que composition. La responsabilité de cela ne peut être imputée au module lui-même.
Solution précédente: // Mauvaises affaires
La première solution que nous avons écrite dans Objective-C, et elle était basée sur NSProxy. Le problème de l'encapsulation du modèle architectural a été résolu par la défenition, qui a été déterminée par les conditions données, c'est-à-dire l'
entrée / sortie du module, qui a permis de proxyer tous les appels vers le module vers son
entrée et de recevoir des messages via la
sortie , le cas échéant.
C'était un pas en avant, mais de nouvelles difficultés sont apparues:
- L'interface proxy ne garantissait pas la mise en œuvre du protocole d' entrée ;
- Les résultats devaient être décrits, même s'ils n'étaient pas nécessaires;
- Il était nécessaire d'ajouter la propriété de sortie à l'interface d' entrée .
En plus de
NSProxy, nous avons également implémenté le routage en examinant l'idée de ViperMcFlurry: nous avons créé une catégorie sur
ViewController , qui a commencé à croître à mesure que différentes options pour afficher le module à l'écran sont apparues. Bien sûr, nous avons divisé la catégorie, mais c'était encore loin d'être une bonne solution.
En général ... la première crêpe est grumeleuse, il est devenu clair que vous devez résoudre le problème différemment.
Solution: // finale
Réalisant qu'il n'y avait
rien de plus avec
NSProxy , nous avons pris des marqueurs et sommes allés tirer. En conséquence, nous avons isolé le protocole
RFModule :
@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } }
Nous avons délibérément abandonné les types associés au niveau du protocole, et il y avait une bonne raison à cela: à cette époque, 90% du code était en Objective-C. L'interopérabilité entre les modules ObjC ← → Swift ne serait pas possible.
Afin d'utiliser toujours des génériques et d'assurer une utilisation typée des modules, nous avons introduit la classe
Module qui satisfait le protocole
RFModule :
final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)?
Nous avons donc obtenu un module tapé. Et en fait, Swift utilise le
module de classe, et dans le
RFModule Objective-C. De plus, il s'est avéré être un outil pratique pour écraser les types à l'endroit où vous devez créer des tableaux: par exemple,
TabContainer .
Étant donné que la création de module DI se trouve dans la
portée UserStory et attribue la valeur de sortie à l'endroit où elle sera utilisée, il est impossible de décrire un simple compteur.
"SetOutput" est, par essence, une
défenition qui, au stade de l'attribution de la
sortie, la transmettra au responsable, selon la logique du 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
La transition est un protocole dont les implémentations, comme son nom l'indique, sont responsables de la logique d'affichage et de masquage du module.
protocol Transitioning { var destination: ViewController? { get }
Pour l'affichage, il est provoqué -
effectuer , pour masquer -
inverser . Malgré le fait qu'il y ait une
destination dans le protocole et au début, il semble qu'il devrait y avoir une
source . En fait, la
source peut ne pas l'être et son type n'est pas toujours
ViewController . Par exemple, si nous avons besoin que le module s'ouvre dans une nouvelle fenêtre, c'est
Window , et si nous avons besoin d'
incorporer , nous avons besoin de ET parent:
ViewController AND conteneur:
UIView .
class PresentTransition: Transitioning { weak var source: ViewController? weak var destination: ViewController? ... func perform(_ completion: (()->())?) { source.present(viewController: self.destinaton) } }
Ainsi, nous nous sommes débarrassés de l'idée d'écrire des extensions sur le
ViewController et
avons décrit la logique de la façon dont nous
affichons nos modules dans divers objets. Cela nous a donné une flexibilité dans le routage, c'est-à-dire maintenant, nous pouvons afficher n'importe quel module à la fois de manière indépendante et complexe, et également varier entre la façon dont tout est affiché à l'écran: dans la fenêtre, Présent, dans la navigation (pousser à la navigation), incorporer, dans le rideau (couverture) .
C'est tout?
Il y a encore une chose qui hante jusqu'à présent. Pour avoir la possibilité de choisir facilement la façon dont le module est affiché et d'en supprimer cette logique, nous avons payé la perte de la possibilité de définir les propriétés d'apparence. Par exemple, si nous le montrons dans Navigation, nous devons spécifier quelle couleur
barTintColor doit être; ou, si nous montrons le module dans le rideau, il est nécessaire de définir la couleur du
gestionnaire .
Jusqu'à présent, nous avons résolu ce problème avec l'apparence non typée: toute propriété, et la transition lors de l'ouverture du module conduit au type avec lequel il fonctionne, et s'il réussit, il enlève les propriétés nécessaires.