J'ai lu sur Clean Architecture de Robert Martin et plus spécifiquement sur VIPER .
Ensuite, je suis tombé sur cet article/article Brigade’s Experience Using an MVC Alternative qui décrit à peu près ce que je fais actuellement.
Après avoir réellement essayé d'implémenter VIPER sur un nouveau projet iOS, je suis tombé sur quelques questions:
Pour répondre à votre satisfaction, nous avons besoin de plus de détails sur le cas particulier. Pourquoi la vue ne peut-elle pas fournir plus d'informations contextuelles directement lors du rappel?
Je vous suggère de passer au présentateur un objet Command afin que le présentateur n'ait pas à savoir quoi faire dans ce cas. Le présentateur peut exécuter la méthode de l'objet, en lui transmettant certaines informations si nécessaire, sans rien savoir de l'état de la vue (et donc en y introduisant un couplage élevé).
id<FollowUpCommand> followUpCommand
. View crée un XFollowUpCommand
(contrairement à YFollowUpCommand
et ZFollowUpCommand
) et définit ses paramètres en conséquence, puis le place dans le DTO.FollowUpCommand
. Il exécute ensuite la seule méthode du protocole, followUpCommand.followUp
. La mise en œuvre concrète saura quoi faire.Si vous devez faire un switch-case/if-else sur une propriété, la plupart du temps cela aiderait à modéliser les options comme des objets héritant d'un protocole commun et à passer les objets au lieu de l'état.
Le module de présentation ou le module présenté doit-il décider s'il est modal? - Le module présenté (le second) devrait décider tant qu'il est conçu pour être utilisé uniquement de manière modale. Mettre la connaissance d'une chose dans la chose elle-même . Si son mode de présentation dépend du contexte, eh bien, le module lui-même ne peut pas décider.
Le filaire du deuxième module recevra un message comme celui-ci:
[secondWireframe presentYourStuffIn:self.viewController]
Le paramètre est l'objet pour lequel la présentation doit avoir lieu. Vous pouvez également transmettre un paramètre asModal
si le module est conçu pour être utilisé dans les deux sens. S'il n'y a qu'une seule façon de le faire, mettez ces informations dans le module affecté (celui présenté) lui-même.
Il fera alors quelque chose comme:
- (void)presentYourStuffIn:(UIViewController)viewController {
// set up module2ViewController
[self.presenter configureUserInterfaceForPresentation:module2ViewController];
// Assuming the modal transition is set up in your Storyboard
[viewController presentViewController:module2ViewController animated:YES completion:nil];
self.presentingViewController = viewController;
}
Si vous utilisez Storyboard Segues, vous devrez faire les choses un peu différemment.
Supposons également que la vue du deuxième module soit poussée dans un contrôleur de navigation, comment l'action "retour" doit-elle être gérée?
Si vous passez "tous VIPER", oui, vous devez passer de la vue à son filaire et vous diriger vers un autre filaire.
Pour retransmettre les données du module présenté ("Second") au module de présentation ("First"), ajoutez SecondDelegate
et implémentez-le dans FirstPresenter
. Avant que le module présenté n'apparaisse, il envoie un message à SecondDelegate
pour notifier le résultat.
"Ne combattez pas le cadre", cependant. Vous pouvez peut-être tirer parti de certaines des subtilités du contrôleur de navigation en sacrifiant la pureté de VIPER. Les séquences sont déjà un pas dans la direction d'un mécanisme de routage. Regardez VTDAddWireframe pour les méthodes UIViewControllerTransitioningDelegate
dans un filaire qui introduisent des animations personnalisées. C'est peut-être utile:
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [[VTDAddDismissalTransition alloc] init];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source
{
return [[VTDAddPresentationTransition alloc] init];
}
J'ai d'abord pensé que vous auriez besoin de conserver une pile de wireframes similaire à la pile de navigation, et que toutes les wireframes du module "actif" sont liées les unes aux autres. Mais ce n'est pas le cas. Les wireframes gèrent le contenu du module, mais la pile de navigation est la seule pile en place représentant quel contrôleur de vue est visible.
Les différents modules doivent-ils parler uniquement à travers le wireframe ou également via les délégués entre les présentateurs?
Si vous envoyez directement à un autre objet du module B un message du présentateur A, que devrait-il se passer alors?
La vue du récepteur n'étant pas visible, une animation ne peut pas démarrer, par exemple. Le présentateur doit encore attendre le filaire/routeur. Il doit donc mettre en file d'attente l'animation jusqu'à ce qu'elle redevienne active. Cela rend le présentateur plus dynamique, ce qui rend son travail plus difficile.
Côté architecture, pensez au rôle que jouent les modules. Dans l'architecture Ports/Adapters, à partir de laquelle Clean Architecture enfonce certains concepts, le problème est plus évident. Par analogie: un ordinateur possède de nombreux ports. Le port USB ne peut pas communiquer avec le port LAN. Chaque flux d'informations doit être acheminé à travers le noyau.
Qu'est-ce qui est au cœur de votre application?
Avez-vous un modèle de domaine? Avez-vous un ensemble de services interrogés à partir de différents modules? Les modules VIPER sont centrés sur la vue. Les modules de partage de choses, comme les mécanismes d'accès aux données, n'appartiennent pas à un module particulier. C'est ce que vous pouvez appeler le noyau. Là, vous devez effectuer des modifications de données. Si un autre module devient visible, il extrait les données modifiées.
À des fins d'animation, cependant, laissez le routeur savoir quoi faire et émettez une commande au présentateur en fonction du changement de module.
Dans l'exemple de code VIPER Todo:
Qui doit conserver l'état de la broche actuellement sélectionnée, le MapViewController, le MapPresenter ou le MapWireframe pour que je sache, au retour, quelle broche doit changer de couleur?
Aucun. Évitez l'état dans vos services de module d'affichage pour réduire les coûts de maintenance de votre code. Au lieu de cela, essayez de déterminer si vous pouvez transmettre une représentation des changements de broches pendant les changements.
Essayez d'atteindre les entités pour obtenir l'état (via le présentateur et l'interacteur et ainsi de suite).
Cela ne signifie pas que vous créez un objet Pin
dans votre couche de vue, le passez d'un contrôleur de vue à un contrôleur de vue, modifiez ses propriétés, puis renvoyez-le pour refléter les modifications. Un NSDictionary
avec des modifications sérialisées ferait-il l'affaire? Vous pouvez y mettre la nouvelle couleur et la renvoyer du PinEditViewController
à son Presenter qui émet un changement dans le MapViewController
.
Maintenant, j'ai triché: MapViewController
doit avoir un état. Il doit connaître toutes les broches. Ensuite, je vous ai suggéré de passer un dictionnaire de modifications pour que MapViewController
sache quoi faire.
Mais comment identifiez-vous la broche affectée?
Chaque broche peut avoir son propre identifiant. Peut-être que cet ID est juste son emplacement sur la carte. C'est peut-être son index dans un tableau de broches. Dans tous les cas, vous avez besoin d'une sorte d'identifiant. Ou vous créez un objet wrapper identifiable qui se maintient sur une broche elle-même pendant la durée de l'opération. (Cela semble trop ridicule pour changer la couleur, cependant.)
VIPER est très basé sur le service. Il y a beaucoup d'objets pour la plupart sans état liés entre eux pour transmettre des messages et transformer des données. Dans le post de Brigade Engineering, une approche centrée sur les données est également présentée.
Les entités sont dans une couche plutôt mince. À l'opposé du spectre que j'ai en tête se trouve un modèle de domaine . Ce modèle n'est pas nécessaire pour toutes les applications. Modéliser le cœur de votre application de manière similaire peut cependant être bénéfique pour répondre à certaines de vos questions.
Contrairement aux Entités en tant que conteneurs de données dans lesquels tout le monde peut accéder via des "gestionnaires de données", un Domaine protège ses Entités. Un domaine informera également les changements de manière proactive. (Par le biais de NSNotificationCenter
, pour commencer. Moins par le biais d'appels de messages directs de type commande.)
Maintenant, cela pourrait également convenir à votre étui Pin:
Pin
est toujours de courte durée, mais c'est toujours une entité parce que son identité compte, pas seulement ses valeurs.)Pin
correspondant a changé de couleur et publie une notification via NSNotificationCenter
.Pin
ne sait pas), certains Interactor s'abonnent à ces notifications et modifient l'apparence de sa vue.Bien que cela puisse fonctionner pour votre cas aussi, je pense que lier la modification
Cette réponse peut être un peu sans rapport, mais je la mets ici pour référence. Le site Clean Swift est une excellente implémentation de " Clean Architecture " d'oncle Bob dans Swift. Le propriétaire l'appelle VIP (il contient quand même les "Entités" et le Routeur/filaire)).
Le site vous propose des modèles XCode. Disons que vous voulez créer une nouvelle scène (il appelle un module VIPER, "scènes"), tout ce que vous faites est File-> new-> sceneTemplate.
Ce modèle crée un lot de 7 fichiers contenant tout le mal de tête du code passe-partout pour votre projet. Il les configure également de sorte qu'ils fonctionnent hors de la boîte. Le site donne une explication assez approfondie de la façon dont tout se combine.
Avec tout le code de la plaque de la chaudière à l'écart, trouver des solutions aux questions que vous avez posées ci-dessus est un peu plus facile. En outre, les modèles permettent une cohérence à tous les niveaux.
[~ # ~] modifier [~ # ~] -> En ce qui concerne les commentaires ci-dessous, voici une explication de la raison pour laquelle je soutiens cette approche -> http://stringerstheory.net/the-clean-er-architecture-for-ios-apps/
Aussi celui-ci -> Le bon, le mauvais et le laid à propos de VIPER dans iOS
La plupart de vos questions trouvent une réponse sur ce post: https://www.ckl.io/blog/best-practices-viper-architecture (exemple de projet inclus). Je vous suggère de porter une attention particulière aux conseils d'initialisation/présentation des modules: c'est à la source Router
de le faire.
En ce qui concerne les boutons de retour, vous pouvez use delegates
pour déclencher ce message vers le module souhaité. Voici comment je le fais et cela fonctionne très bien (même après avoir inséré des notifications Push).
Et oui, les modules peuvent certainement se parler entre using delegates
ainsi que. C'est un must pour les projets plus complexes.