Il est bien connu dans le domaine de l’architecture que nous façonnons nos bâtiments, et qu’ensuite nos bâtiments nous façonnent. Comme tous les programmeurs finissent par l’apprendre, cela s’applique tout aussi bien à la construction de logiciels.
Il est important de concevoir notre code de façon à ce que chaque pièce soit facilement identifiable, ait un but spécifique et évident, et s’intègre aux autres pièces de façon logique. C’est ce que nous appelons l’architecture logicielle. Une bonne architecture n’est pas ce qui fait le succès d’un produit, mais elle le rend maintenable et aide à préserver la santé mentale des personnes qui le maintiennent !
Dans cet article, nous allons présenter une approche de l’architecture des applications iOS appelée VIPER. VIPER a été utilisé pour construire de nombreux grands projets, mais pour les besoins de cet article, nous allons vous montrer VIPER en construisant une application de liste de choses à faire. Vous pouvez suivre le projet d’exemple ici sur GitHub:
- Qu’est-ce que VIPER?
- Conception d’applications basée sur des cas d’utilisation
- Principales parties de VIPER
- Interacteur
- Entity
- Presenter
- Vue
- Routage
- Composants d’application s’adaptant à VIPER
- Utilisation de VIPER pour construire des modules
- Tester avec VIPER
- Conclusion
- Swift Addendum
- Structs
- Type Safety
- Lecture complémentaire
Qu’est-ce que VIPER?
Les tests n’ont pas toujours été une partie importante de la construction d’applications iOS. Alors que nous nous sommes lancés dans une quête pour améliorer nos pratiques de test chez Mutual Mobile, nous avons constaté que l’écriture de tests pour les apps iOS était difficile. Nous avons décidé que si nous voulions améliorer la façon dont nous testons nos logiciels, nous devions d’abord trouver une meilleure façon d’architecturer nos applications. Nous appelons cette méthode VIPER.
VIPER est une application de l’architecture propre aux applications iOS. Le mot VIPER est un backronym pour View, Interactor, Presenter, Entity, and Routing. L’architecture propre divise la structure logique d’une application en couches distinctes de responsabilité. Cela permet d’isoler plus facilement les dépendances (par exemple, votre base de données) et de tester les interactions aux frontières entre les couches :
La plupart des applications iOS sont architecturées à l’aide de MVC (model-view-controller). L’utilisation de MVC comme architecture d’application peut vous guider à penser que chaque classe est soit un modèle, soit une vue, soit un contrôleur. Comme une grande partie de la logique applicative n’a pas sa place dans un modèle ou une vue, elle se retrouve généralement dans le contrôleur. Cela conduit à un problème connu sous le nom de contrôleur de vue massif, où les contrôleurs de vue finissent par en faire trop. L’amincissement de ces contrôleurs de vue massifs n’est pas le seul défi auquel sont confrontés les développeurs iOS qui cherchent à améliorer la qualité de leur code, mais c’est un excellent point de départ.
Les couches distinctes de VIPER permettent de relever ce défi en fournissant des emplacements clairs pour la logique applicative et le code lié à la navigation. Avec VIPER appliqué, vous remarquerez que les contrôleurs de vue dans notre exemple de liste de tâches sont des machines de contrôle de vue maigres et méchantes. Vous constaterez également que le code des contrôleurs de vue et de toutes les autres classes est facile à comprendre, plus facile à tester et, par conséquent, également plus facile à maintenir.
Conception d’applications basée sur des cas d’utilisation
Les applications sont souvent mises en œuvre comme un ensemble de cas d’utilisation. Les cas d’utilisation sont également connus sous le nom de critères d’acceptation, ou de comportements, et décrivent ce qu’une app est censée faire. Peut-être qu’une liste doit pouvoir être triée par date, type ou nom. C’est un cas d’utilisation. Un cas d’utilisation est la couche d’une application qui est responsable de la logique commerciale. Les cas d’utilisation doivent être indépendants de leur mise en œuvre dans l’interface utilisateur. Ils doivent également être petits et bien définis. Décider comment décomposer une app complexe en cas d’utilisation plus petits est un défi et nécessite de la pratique, mais c’est un moyen utile de limiter la portée de chaque problème que vous résolvez et de chaque classe que vous écrivez.
Construire une app avec VIPER implique la mise en œuvre d’un ensemble de composants pour remplir chaque cas d’utilisation. La logique applicative est une partie importante de la mise en œuvre d’un cas d’utilisation, mais ce n’est pas la seule partie. Le cas d’utilisation affecte également l’interface utilisateur. De plus, il est important de considérer comment le cas d’utilisation s’intègre à d’autres composants fondamentaux d’une application, tels que la mise en réseau et la persistance des données. Les composants agissent comme des plugins pour les cas d’utilisation, et VIPER est un moyen de décrire quel est le rôle de chacun de ces composants et comment ils peuvent interagir les uns avec les autres.
L’un des cas d’utilisation ou des exigences pour notre application de liste de tâches était de regrouper les tâches de différentes manières en fonction de la sélection d’un utilisateur. En séparant la logique qui organise ces données dans un cas d’utilisation, nous sommes en mesure de garder le code de l’interface utilisateur propre et d’envelopper facilement le cas d’utilisation dans des tests pour s’assurer qu’il continue à fonctionner comme nous l’attendons.
Principales parties de VIPER
Les principales parties de VIPER sont :
- Vue : affiche ce que lui dit le présentateur et retransmet les entrées de l’utilisateur au présentateur.
- Interacteur : contient la logique métier telle que spécifiée par un cas d’utilisation.
- Présentateur : contient la logique de vue pour préparer le contenu pour l’affichage (tel que reçu de l’Interacteur) et pour réagir aux entrées de l’utilisateur (en demandant de nouvelles données à l’Interacteur).
- Entité : contient les objets de modèle de base utilisés par l’Interacteur.
- Routage : contient la logique de navigation pour décrire quels écrans sont montrés dans quel ordre.
Cette séparation est également conforme au principe de responsabilité unique. L’Interacteur est responsable de l’analyste métier, le Présentateur représente le concepteur d’interaction, et la Vue est responsable du concepteur visuel.
Vous trouverez ci-dessous un diagramme des différents composants et de la façon dont ils sont connectés :
Bien que les composants de VIPER puissent être mis en œuvre dans une application dans n’importe quel ordre, nous avons choisi de présenter les composants dans l’ordre dans lequel nous recommandons de les mettre en œuvre. Vous remarquerez que cet ordre est à peu près cohérent avec le processus de construction d’une application entière, qui commence par discuter de ce que le produit doit faire, puis de la façon dont un utilisateur interagira avec lui.
Interacteur
Un Interacteur représente un cas d’utilisation unique dans l’app. Il contient la logique métier permettant de manipuler les objets du modèle (Entités) pour réaliser une tâche spécifique. Le travail effectué dans un Interactor doit être indépendant de toute interface utilisateur. Le même Interactor pourrait être utilisé dans une application iOS ou une application OS X.
Parce que l’Interactor est un PONSO (Plain Old NSObject
) qui contient principalement de la logique, il est facile à développer en utilisant le TDD.
Le cas d’utilisation principal de l’exemple d’application est de montrer à l’utilisateur tous les éléments à faire à venir (c’est-à-dire tout ce qui est dû avant la fin de la semaine prochaine). La logique métier pour ce cas d’utilisation est de trouver tous les éléments à faire dus entre aujourd’hui et la fin de la semaine prochaine et d’attribuer une date d’échéance relative : aujourd’hui, demain, plus tard cette semaine ou la semaine prochaine.
Vous trouverez ci-dessous la méthode correspondante de VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Les entités sont les objets du modèle manipulés par un Interactor. Les entités sont uniquement manipulées par l’Interacteur. L’Interacteur ne passe jamais les entités à la couche de présentation (c’est-à-dire le Présentateur).
Les entités ont également tendance à être des PONSO. Si vous utilisez Core Data, vous voudrez que vos objets gérés restent derrière votre couche de données. Les interacteurs ne devraient pas travailler avec NSManagedObjects
.
Voici l’entité pour notre élément à faire:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Ne soyez pas surpris si vos entités sont juste des structures de données. Toute logique dépendante de l’application sera très probablement dans un Interactor.
Presenter
Le Presenter est un PONSO qui consiste principalement en une logique pour piloter l’interface utilisateur. Il sait quand présenter l’interface utilisateur. Il recueille les entrées des interactions de l’utilisateur afin de pouvoir mettre à jour l’IU et envoyer des demandes à un Interacteur.
Lorsque l’utilisateur tape sur le bouton + pour ajouter un nouvel élément à faire, addNewEntry
est appelé. Pour cette action, le présentateur demande au wireframe de présenter l’interface utilisateur pour ajouter un nouvel élément:
- (void)addNewEntry{ ;}
Le présentateur reçoit également des résultats d’un Interacteur et convertit les résultats en un formulaire efficace à afficher dans une vue.
Vous trouverez ci-dessous la méthode qui reçoit les éléments à venir de l’Interacteur. Elle traitera les données et déterminera ce qu’il faut montrer à l’utilisateur :
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Les entités ne sont jamais transmises de l’Interacteur au Présentateur. Au lieu de cela, de simples structures de données qui n’ont aucun comportement sont transmises de l’Interacteur au Présentateur. Cela empêche tout « vrai travail » d’être effectué dans le Presenter. Le Presenter peut seulement préparer les données pour l’affichage dans la vue.
Vue
La vue est passive. Elle attend que le Présentateur lui donne du contenu à afficher ; elle ne demande jamais de données au Présentateur. Les méthodes définies pour une Vue (par exemple LoginView pour un écran de connexion) doivent permettre à un Présentateur de communiquer à un niveau d’abstraction plus élevé, exprimé en termes de son contenu, et non de la manière dont ce contenu doit être affiché. Le présentateur ne connaît pas l’existence de UILabel
, UIButton
, etc. Le Présentateur ne connaît que le contenu qu’il gère et le moment où il doit être affiché. C’est à la vue de déterminer comment le contenu est affiché.
La vue est une interface abstraite, définie en Objective-C avec un protocole. Une UIViewController
ou une de ses sous-classes implémentera le protocole View. Par exemple, l’écran » ajouter » de notre exemple possède l’interface suivante :
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
Les vues et les contrôleurs de vues gèrent également les interactions et les entrées de l’utilisateur. Il est facile de comprendre pourquoi les contrôleurs de vue deviennent généralement si grands, puisqu’ils sont l’endroit le plus facile pour traiter cette entrée pour effectuer une certaine action. Pour que nos contrôleurs de vue restent légers, nous devons leur donner un moyen d’informer les parties intéressées lorsqu’un utilisateur effectue certaines actions. Le contrôleur de vue ne devrait pas prendre de décisions basées sur ces actions, mais il devrait transmettre ces événements à quelque chose qui le peut.
Dans notre exemple, le contrôleur de vue d’ajout a une propriété de gestionnaire d’événements qui se conforme à l’interface suivante :
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
Lorsque l’utilisateur tape sur le bouton d’annulation, le contrôleur de vue indique à ce gestionnaire d’événements que l’utilisateur a indiqué qu’il devait annuler l’action d’ajout. De cette façon, le gestionnaire d’événements peut se charger de congédier le contrôleur de vue d’ajout et de dire à la vue de liste de se mettre à jour.
La frontière entre la vue et le présentateur est également un endroit idéal pour ReactiveCocoa. Dans cet exemple, le contrôleur de vue pourrait également fournir des méthodes pour renvoyer des signaux qui représentent des actions de bouton. Cela permettrait au Présentateur de répondre facilement à ces signaux sans rompre la séparation des responsabilités.
Routage
Les routes d’un écran à l’autre sont définies dans les wireframes créés par un designer d’interaction. Dans VIPER, la responsabilité du routage est partagée entre deux objets : le présentateur, et le wireframe. Un objet wireframe possède les UIWindow
, UINavigationController
, UIViewController
, etc. Il est responsable de la création d’une vue/un contrôleur de vue et de son installation dans la fenêtre.
Puisque le présentateur contient la logique pour réagir aux entrées de l’utilisateur, c’est le présentateur qui sait quand naviguer vers un autre écran, et vers quel écran naviguer. Pendant ce temps, le fil de fer sait comment naviguer. Le présentateur utilisera donc le fil de fer pour effectuer la navigation. Ensemble, ils décrivent un itinéraire d’un écran à l’autre.
Le wireframe est également un endroit évident pour gérer les animations de transition de navigation. Regardez cet exemple à partir du wireframe d’ajout :
@implementation VTDAddWireframe- (void)presentAddInterfaceFromViewController:(UIViewController *)viewController { VTDAddViewController *addViewController = ; addViewController.eventHandler = self.addPresenter; addViewController.modalPresentationStyle = UIModalPresentationCustom; addViewController.transitioningDelegate = self; ; self.presentedViewController = viewController;}#pragma mark - UIViewControllerTransitioningDelegate Methods- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return init];}- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return init];}@end
L’application utilise une transition de contrôleur de vue personnalisée pour présenter le contrôleur de vue d’ajout. Puisque le wireframe est responsable de l’exécution de la transition, il devient le délégué de transition pour le contrôleur de vue add et peut renvoyer les animations de transition appropriées.
Composants d’application s’adaptant à VIPER
Une architecture d’application iOS doit tenir compte du fait que UIKit et Cocoa Touch sont les principaux outils sur lesquels les apps sont construites. L’architecture doit coexister pacifiquement avec tous les composants de l’application, mais elle doit également fournir des lignes directrices sur la façon dont certaines parties des frameworks sont utilisées et où elles vivent.
Le cheval de bataille d’une application iOS est UIViewController
. Il serait facile de supposer qu’un prétendant au remplacement de MVC hésiterait à faire un usage intensif des contrôleurs de vue. Mais les contrôleurs de vue sont au cœur de la plateforme : ils gèrent les changements d’orientation, répondent aux entrées de l’utilisateur, s’intègrent bien aux composants du système comme les contrôleurs de navigation, et maintenant avec iOS 7, permettent des transitions personnalisables entre les écrans. Ils sont extrêmement utiles.
Avec VIPER, un contrôleur de vue fait exactement ce pour quoi il a été conçu : il contrôle la vue. Notre application de liste de tâches a deux contrôleurs de vue, un pour l’écran de liste, et un pour l’écran d’ajout. L’implémentation du contrôleur de vue d’ajout est extrêmement basique car tout ce qu’il doit faire est de contrôler la vue:
@implementation VTDAddViewController- (void)viewDidAppear:(BOOL)animated { ; UITapGestureRecognizer *gestureRecognizer = initWithTarget:self action:@selector(dismiss)]; ; self.transitioningBackgroundView.userInteractionEnabled = YES;}- (void)dismiss { ;}- (void)setEntryName:(NSString *)name { self.nameTextField.text = name;}- (void)setEntryDueDate:(NSDate *)date { ;}- (IBAction)save:(id)sender { ;}- (IBAction)cancel:(id)sender { ;}#pragma mark - UITextFieldDelegate Methods- (BOOL)textFieldShouldReturn:(UITextField *)textField { ; return YES;}@end
Les applications sont généralement beaucoup plus convaincantes lorsqu’elles sont connectées au réseau. Mais où cette mise en réseau doit-elle avoir lieu et qui doit être responsable de l’initier ? C’est généralement à l’Interactor d’initier une opération de mise en réseau, mais il ne gérera pas directement le code de mise en réseau. Il demandera à une dépendance, comme un gestionnaire de réseau ou un client API. L’Interacteur peut avoir à agréger des données provenant de plusieurs sources pour fournir les informations nécessaires à la réalisation d’un cas d’utilisation. Ensuite, c’est au présentateur de prendre les données renvoyées par l’Interactor et de les formater pour la présentation.
Un magasin de données est chargé de fournir des entités à un Interactor. Lorsqu’un Interactor applique sa logique métier, il devra récupérer des entités dans le magasin de données, manipuler les entités, puis remettre les entités mises à jour dans le magasin de données. Le magasin de données gère la persistance des entités. Les entités ne connaissent pas le magasin de données, donc les entités ne savent pas comment se persister.
L’Interacteur ne devrait pas non plus savoir comment persister les entités. Parfois, l’Interactor peut vouloir utiliser un type d’objet appelé gestionnaire de données pour faciliter son interaction avec le magasin de données. Le gestionnaire de données gère davantage d’opérations spécifiques au magasin, comme la création de requêtes d’extraction, la construction de requêtes, etc. Cela permet à l’Interactor de se concentrer davantage sur la logique de l’application et de ne pas avoir à connaître la manière dont les entités sont rassemblées ou conservées. Un exemple de cas où il est judicieux d’utiliser un gestionnaire de données est lorsque vous utilisez Core Data, qui est décrit ci-dessous.
Voici l’interface du gestionnaire de données de l’application d’exemple:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
Lorsque vous utilisez TDD pour développer un Interactor, il est possible de remplacer le magasin de données de production par un double/mock de test. Ne pas parler à un serveur distant (pour un service web) ou ne pas toucher le disque (pour une base de données) permet à vos tests d’être plus rapides et plus répétables.
Une raison de garder le magasin de données comme une couche distincte avec des limites claires est que cela vous permet de retarder le choix d’une technologie de persistance spécifique. Si votre magasin de données est une classe unique, vous pouvez démarrer votre application avec une stratégie de persistance de base, puis passer à SQLite ou Core Data plus tard si et quand il est judicieux de le faire, le tout sans changer quoi que ce soit d’autre dans la base de code de votre application.
L’utilisation de Core Data dans un projet iOS peut souvent susciter plus de débats que l’architecture elle-même. Cependant, l’utilisation de Core Data avec VIPER peut être la meilleure expérience de Core Data que vous ayez jamais eue. Core Data est un excellent outil pour la persistance des données tout en maintenant un accès rapide et une faible empreinte mémoire. Mais il a l’habitude de serpenter ses vrilles NSManagedObjectContext
partout dans les fichiers d’implémentation d’une application, en particulier là où ils ne devraient pas être. VIPER garde les données de base là où elles doivent être : au niveau de la couche de stockage des données.
Dans l’exemple de la liste de tâches, les deux seules parties de l’app qui savent que Core Data est utilisé sont le magasin de données lui-même, qui configure la pile Core Data, et le gestionnaire de données. Le gestionnaire de données exécute une requête de récupération, convertit les NSManagedObjects
renvoyés par le magasin de données en objets de modèle PONSO standard, et les renvoie à la couche de logique métier. De cette façon, le cœur de l’application n’est jamais dépendant de Core Data et, en prime, vous n’avez jamais à vous préoccuper d’un NSManagedObjects
artifice périmé ou mal threadé.
Voici à quoi cela ressemble à l’intérieur du gestionnaire de données lorsqu’une demande est faite pour accéder au magasin de Core Data :
@implementation VTDListDataManager- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate*)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock{ NSCalendar *calendar = ; NSPredicate *predicate = , ]; NSArray *sortDescriptors = @; __weak typeof(self) welf = self; ); } }];}- (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries{ return ; }];}@end
Presque aussi controversés que Core Data sont les Storyboards d’interface utilisateur. Les Storyboards ont de nombreuses fonctionnalités utiles, et les ignorer entièrement serait une erreur. Cependant, il est difficile d’accomplir tous les objectifs de VIPER tout en employant toutes les fonctionnalités qu’un storyboard a à offrir.
Le compromis que nous avons tendance à faire est de choisir de ne pas utiliser les segues. Il peut y avoir des cas où l’utilisation de la segue a du sens, mais le danger avec les segues est qu’ils rendent très difficile de garder la séparation entre les écrans – ainsi qu’entre l’interface utilisateur et la logique de l’application – intacte. En règle générale, nous essayons de ne pas utiliser de segues si la mise en œuvre de la méthode prepareForSegue semble nécessaire.
Autrement, les storyboards sont un excellent moyen de mettre en œuvre la mise en page de votre interface utilisateur, en particulier lors de l’utilisation de la mise en page automatique. Nous avons choisi d’implémenter les deux écrans de l’exemple de la liste de tâches à l’aide d’un storyboard, et d’utiliser un code tel que celui-ci pour effectuer notre propre navigation :
static NSString *ListViewControllerIdentifier = @"VTDListViewController";@implementation VTDListWireframe- (void)presentListInterfaceFromWindow:(UIWindow *)window { VTDListViewController *listViewController = ; listViewController.eventHandler = self.listPresenter; self.listPresenter.userInterface = listViewController; self.listViewController = listViewController; ;}- (VTDListViewController *)listViewControllerFromStoryboard { UIStoryboard *storyboard = ; VTDListViewController *viewController = ; return viewController;}- (UIStoryboard *)mainStoryboard { UIStoryboard *storyboard = ]; return storyboard;}@end
Utilisation de VIPER pour construire des modules
Souvent, lorsque vous travaillez avec VIPER, vous constaterez qu’un écran ou un ensemble d’écrans a tendance à se rassembler sous forme de module. Un module peut être décrit de plusieurs façons, mais généralement, il est préférable de le considérer comme une fonctionnalité. Dans une application de podcasting, un module peut être le lecteur audio ou le navigateur d’abonnement. Dans notre application de liste de tâches, les écrans de liste et d’ajout sont chacun construits comme des modules séparés.
Il y a quelques avantages à concevoir votre application comme un ensemble de modules. L’un est que les modules peuvent avoir des interfaces très claires et bien définies, ainsi qu’être indépendants des autres modules. Cela rend beaucoup plus facile l’ajout/la suppression de fonctionnalités, ou la modification de la façon dont votre interface présente les différents modules à l’utilisateur.
Nous voulions rendre la séparation entre les modules très claire dans l’exemple de la liste de tâches, nous avons donc défini deux protocoles pour le module d’ajout. Le premier est l’interface du module, qui définit ce que le module peut faire. Le second est le délégué du module, qui décrit ce que le module a fait. Exemple:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Puisqu’un module doit être présenté pour avoir beaucoup de valeur pour l’utilisateur, le Presenter du module implémente généralement l’interface du module. Lorsqu’un autre module veut présenter celui-ci, son Présentateur implémentera le protocole de délégué de module, afin qu’il sache ce que le module a fait pendant qu’il était présenté.
Un module pourrait inclure une couche logique d’application commune d’entités, d’interacteurs et de gestionnaires qui peut être utilisée pour plusieurs écrans. Ceci, bien sûr, dépend de l’interaction entre ces écrans et de leur similarité. Un module pourrait tout aussi bien ne représenter qu’un seul écran, comme le montre l’exemple de la liste de tâches. Dans ce cas, la couche logique d’application peut être très spécifique au comportement de son module particulier.
Les modules sont également juste un bon moyen simple d’organiser le code. Garder tout le code d’un module rangé dans son propre dossier et groupe dans Xcode permet de le retrouver facilement lorsque vous devez modifier quelque chose. C’est un sentiment formidable lorsque vous trouvez une classe exactement là où vous vous attendiez à la chercher.
Un autre avantage à construire des modules avec VIPER est qu’ils deviennent plus faciles à étendre à plusieurs facteurs de forme. Le fait d’avoir la logique d’application pour tous vos cas d’utilisation isolés au niveau de la couche Interactor vous permet de vous concentrer sur la construction de la nouvelle interface utilisateur pour tablette, téléphone ou Mac, tout en réutilisant votre couche d’application.
Pour aller plus loin, l’interface utilisateur des applications iPad peut être en mesure de réutiliser certaines des vues, des contrôleurs de vue et des présentateurs de l’application iPhone. Dans ce cas, un écran iPad serait représenté par des « super » présentateurs et des wireframes, qui composeraient l’écran en utilisant les présentateurs et wireframes existants qui ont été écrits pour l’iPhone. La construction et la maintenance d’une application sur plusieurs plateformes peuvent être assez difficiles, mais une bonne architecture qui favorise la réutilisation à travers le modèle et la couche d’application aide à rendre cela beaucoup plus facile.
Tester avec VIPER
Suivre VIPER encourage une séparation des préoccupations qui facilite l’adoption du TDD. L’Interactor contient une logique pure, indépendante de toute interface utilisateur, ce qui facilite le pilotage par les tests. Le Presenter contient une logique pour préparer les données à l’affichage et est indépendant de tout widget UIKit. Le développement de cette logique est également facile à piloter avec des tests.
Notre méthode préférée est de commencer par l’Interactor. Tout dans l’interface utilisateur est là pour répondre aux besoins du cas d’utilisation. En utilisant TDD pour tester l’API pour l’Interacteur, vous aurez une meilleure compréhension de la relation entre l’UI et le cas d’utilisation.
À titre d’exemple, nous allons examiner l’Interacteur responsable de la liste des éléments à faire à venir. La politique de recherche des éléments à venir consiste à trouver tous les éléments à faire dus avant la fin de la semaine prochaine et à classer chaque élément à faire comme étant dû aujourd’hui, demain, plus tard cette semaine ou la semaine prochaine.
Le premier test que nous écrivons consiste à s’assurer que l’Interacteur trouve tous les éléments à faire dus à la fin de la semaine prochaine:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
Une fois que nous savons que l’Interacteur demande les éléments à faire appropriés, nous écrirons plusieurs tests pour confirmer qu’il attribue les éléments à faire au bon groupe de dates relatives (par ex.g. aujourd’hui, demain, etc.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Maintenant que nous savons à quoi ressemble l’API pour l’Interactor, nous pouvons développer le Presenter. Lorsque le Présentateur reçoit des éléments de tâches à venir de l’Interacteur, nous voudrons tester que nous formatons correctement les données et les affichons dans l’interface utilisateur:
- (void)testFoundZeroUpcomingItemsDisplaysNoContentMessage{ showNoContentMessage]; ];}- (void)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay{ VTDUpcomingDisplayData *displayData = ; showUpcomingDisplayData:displayData]; NSCalendar *calendar = ; NSDate *dueDate = ; VTDUpcomingItem *haircut = ; ];}- (void)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay{ VTDUpcomingDisplayData *displayData = ; showUpcomingDisplayData:displayData]; NSCalendar *calendar = ; NSDate *dueDate = ; VTDUpcomingItem *groceries = ; ];}
Nous voulons également tester que l’application démarre l’action appropriée lorsque l’utilisateur veut ajouter un nouvel élément de tâche:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
Nous pouvons maintenant développer la Vue. Lorsqu’il n’y a pas d’éléments à faire à venir, nous voulons montrer un message spécial:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
Quand il y a des éléments à faire à venir à afficher, nous voulons nous assurer que le tableau s’affiche:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Construire l’Interacteur en premier est un ajustement naturel avec TDD. Si vous développez d’abord l’Interactor, puis le Presenter, vous pouvez construire une suite de tests autour de ces couches et poser les bases de l’implémentation de ces cas d’utilisation. Vous pouvez itérer rapidement sur ces classes, car vous n’aurez pas à interagir avec l’interface utilisateur pour les tester. Ensuite, lorsque vous développerez la vue, vous disposerez d’une logique fonctionnelle et testée et d’une couche de présentation à laquelle vous pourrez vous connecter. Au moment où vous terminez le développement de la vue, vous pourriez constater que la première fois que vous exécutez l’application, tout fonctionne simplement, parce que tous vos tests qui passent vous disent que cela va fonctionner.
Conclusion
Nous espérons que vous avez apprécié cette introduction à VIPER. Beaucoup d’entre vous se demandent peut-être maintenant où aller ensuite. Si vous vouliez architecturer votre prochaine application en utilisant VIPER, par où commenceriez-vous ?
Cet article et notre exemple de mise en œuvre d’une application utilisant VIPER sont aussi spécifiques et bien définis que nous pouvions les faire. Notre application de liste de tâches est plutôt simple, mais elle devrait également expliquer avec précision comment construire une application en utilisant VIPER. Dans un projet réel, le degré de conformité à cet exemple dépendra de vos propres défis et contraintes. Selon notre expérience, chacun de nos projets a légèrement varié l’approche adoptée pour utiliser VIPER, mais tous ont grandement bénéficié de son utilisation pour guider leurs approches.
Il peut y avoir des cas où vous souhaitez dévier du chemin tracé par VIPER pour diverses raisons. Peut-être êtes-vous tombé sur une garenne d’objets » lapin « , ou votre application bénéficierait de l’utilisation de séquences dans les Storyboards. Ce n’est pas grave. Dans ces cas, tenez compte de l’esprit de ce que VIPER représente lorsque vous prenez votre décision. Au fond, VIPER est une architecture basée sur le principe de la responsabilité unique. Si vous rencontrez des difficultés, pensez à ce principe lorsque vous décidez de la marche à suivre.
Vous vous demandez peut-être aussi s’il est possible d’utiliser VIPER dans votre application existante. Dans ce scénario, envisagez de construire une nouvelle fonctionnalité avec VIPER. Beaucoup de nos projets existants ont emprunté cette voie. Cela vous permet de construire un module à l’aide de VIPER, et vous aide également à repérer les problèmes existants qui pourraient rendre plus difficile l’adoption d’une architecture basée sur le principe de responsabilité unique.
L’un des grands avantages du développement de logiciels est que chaque app est différente, et qu’il existe également différentes façons d’architecturer toute app. Pour nous, cela signifie que chaque application est une nouvelle opportunité d’apprendre et d’essayer de nouvelles choses. Si vous décidez d’essayer VIPER, nous pensons que vous apprendrez aussi quelques nouvelles choses. Merci de votre lecture.
Swift Addendum
La semaine dernière, lors de la WWDC, Apple a présenté le langage de programmation Swift comme l’avenir du développement Cocoa et Cocoa Touch. Il est trop tôt pour avoir formé des opinions complexes sur le langage Swift, mais nous savons que les langages ont une influence majeure sur la façon dont nous concevons et construisons les logiciels. Nous avons décidé de réécrire notre application d’exemple VIPER TODO en utilisant Swift pour nous aider à apprendre ce que cela signifie pour VIPER. Jusqu’à présent, nous aimons ce que nous voyons. Voici quelques fonctionnalités de Swift qui, selon nous, amélioreront l’expérience de la construction d’applications utilisant VIPER.
Structs
Dans VIPER, nous utilisons de petites classes de modèle légères pour faire passer les données entre les couches, par exemple du présentateur à la vue. Ces PONSO sont généralement destinés à simplement transporter de petites quantités de données, et ne sont généralement pas destinés à être sous-classés. Les structures Swift sont parfaitement adaptées à ces situations. Voici un exemple d’une structure utilisée dans l’exemple Swift de VIPER. Remarquez que cette struct doit être équatable, et nous avons donc surchargé l’opérateur == pour comparer deux instances de son type :
struct UpcomingDisplayItem : Equatable, Printable { let title : String = "" let dueDate : String = "" var description : String { get { return "\(title) -- \(dueDate)" }} init(title: String, dueDate: String) { self.title = title self.dueDate = dueDate }}func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool { var hasEqualSections = false hasEqualSections = rightSide.title == leftSide.title if hasEqualSections == false { return false } hasEqualSections = rightSide.dueDate == rightSide.dueDate return hasEqualSections}
Type Safety
Peut-être que la plus grande différence entre Objective-C et Swift est la façon dont les deux traitent les types. Objective-C est typée dynamiquement et Swift est très intentionnellement strict avec la façon dont il implémente la vérification des types au moment de la compilation. Dans le cas d’une architecture telle que VIPER, où une application est composée de plusieurs couches distinctes, la sécurité des types peut être un avantage considérable pour l’efficacité du programmeur et la structure architecturale. Le compilateur vous aide à vous assurer que les conteneurs et les objets sont du bon type lorsqu’ils sont transmis entre les couches. C’est l’endroit idéal pour utiliser les structures comme indiqué ci-dessus. Si un struct est destiné à vivre à la frontière entre deux couches, alors vous pouvez garantir qu’il ne pourra jamais s’échapper d’entre ces couches grâce à la sécurité de type.
Lecture complémentaire
- VIPER TODO, article exemple d’app
- VIPER SWIFT, article exemple d’app construit avec Swift
- Counter, autre exemple d’app
- Mutual Mobile Introduction to VIPER
- Clean Architecture
- Lighter View Controllers
- Testing View Controllers
- Bunnies
.