In de architectuur is het algemeen bekend dat wij onze gebouwen vormen, en dat onze gebouwen vervolgens ons vormen. Zoals alle programmeurs uiteindelijk leren, geldt dit net zo goed voor het bouwen van software.
Het is belangrijk om onze code zo te ontwerpen dat elk onderdeel gemakkelijk te herkennen is, een specifiek en duidelijk doel heeft, en op een logische manier in elkaar past met andere onderdelen. Dit noemen we software architectuur. Een goede architectuur is niet wat een product succesvol maakt, maar het maakt een product wel onderhoudbaar en helpt het verstand van de mensen die het onderhouden te bewaren!
In dit artikel introduceren we een benadering van iOS applicatie architectuur genaamd VIPER. VIPER is gebruikt om veel grote projecten te bouwen, maar voor de doeleinden van dit artikel zullen we VIPER laten zien door het bouwen van een to-do lijst app. Je kunt het voorbeeld project hier volgen op GitHub:
Wat is VIPER?
Testen was niet altijd een belangrijk onderdeel van het bouwen van iOS apps. Toen we bij Mutual Mobile begonnen met het verbeteren van onze testmethoden, kwamen we erachter dat het schrijven van tests voor iOS-apps moeilijk was. We besloten dat als we de manier waarop we onze software testen wilden verbeteren, we eerst een betere manier moesten bedenken om onze apps te ontwerpen. Die methode noemen we VIPER.
VIPER is een toepassing van Clean Architecture op iOS apps. Het woord VIPER is een backronym voor View, Interactor, Presenter, Entity, en Routing. Schone Architectuur verdeelt de logische structuur van een app in verschillende lagen van verantwoordelijkheid. Dit maakt het eenvoudiger om afhankelijkheden te isoleren (bijvoorbeeld je database) en om de interacties op de grenzen tussen de lagen te testen:
De meeste iOS-apps zijn opgebouwd volgens MVC (model-view-controller). Het gebruik van MVC als applicatiearchitectuur kan ertoe leiden dat je denkt dat elke klasse ofwel een model, ofwel een view, ofwel een controller is. Omdat veel van de applicatielogica niet thuishoort in een model of een view, komt het meestal terecht in de controller. Dit leidt tot een probleem dat bekend staat als een Massive View Controller, waar de view controllers uiteindelijk te veel doen. Het afslanken van deze massieve view controllers is niet de enige uitdaging voor iOS ontwikkelaars die de kwaliteit van hun code willen verbeteren, maar het is een goede plek om te beginnen.
VIPER’s verschillende lagen helpen bij het omgaan met deze uitdaging door duidelijke locaties te bieden voor applicatielogica en navigatie-gerelateerde code. Met VIPER toegepast, zult u merken dat de view controllers in onze to-do lijst voorbeeld zijn lean, mean, view controlling machines. Je zult ook merken dat de code in de view controllers en alle andere classes makkelijk te begrijpen is, makkelijker te testen, en als gevolg daarvan ook makkelijker te onderhouden.
Applicatie-ontwerp gebaseerd op Use Cases
Apps worden vaak geïmplementeerd als een set van use cases. Use cases zijn ook bekend als acceptatiecriteria, of gedrag, en beschrijven wat een app moet doen. Misschien moet een lijst sorteerbaar zijn op datum, type, of naam. Dat is een use-case. Een use case is de laag van een applicatie die verantwoordelijk is voor de business logica. Use cases moeten onafhankelijk zijn van de gebruikersinterface die ze implementeert. Ze moeten ook klein en goed gedefinieerd zijn. Beslissen hoe je een complexe app opdeelt in kleinere use cases is een uitdaging en vereist oefening, maar het is een handige manier om de reikwijdte te beperken van elk probleem dat je oplost en elke klasse die je schrijft.
Het bouwen van een app met VIPER omvat het implementeren van een set componenten om elke use case te vervullen. Applicatie logica is een belangrijk onderdeel van het implementeren van een use case, maar het is niet het enige onderdeel. De use case heeft ook invloed op de gebruikersinterface. Bovendien is het belangrijk om te overwegen hoe de use-case past bij andere kerncomponenten van een applicatie, zoals netwerken en datapersistentie. Componenten fungeren als plugins voor de use cases, en VIPER is een manier om te beschrijven wat de rol van elk van deze componenten is en hoe ze met elkaar kunnen interageren.
Eén van de use cases of requirements voor onze to-do lijst app was om de to-dos op verschillende manieren te groeperen op basis van de selectie van een gebruiker. Door het scheiden van de logica die die gegevens organiseert in een use case, zijn we in staat om de gebruikersinterface code schoon te houden en gemakkelijk wikkel de use case in tests om ervoor te zorgen dat het blijft werken op de manier waarop we verwachten dat het werkt.
Belangrijkste onderdelen van VIPER
De belangrijkste onderdelen van VIPER zijn:
- View: toont wat de Presenter hem opdraagt en stuurt gebruikersinvoer terug naar de Presenter.
- Interactor: bevat de business logica zoals gespecificeerd door een use-case.
- Presenter: bevat view logica voor het voorbereiden van inhoud voor weergave (zoals ontvangen van de Interactor) en voor het reageren op gebruikersinvoer (door nieuwe gegevens van de Interactor op te vragen).
- Entity: bevat basis model objecten gebruikt door de Interactor.
- Routing: bevat navigatie logica voor het beschrijven welke schermen in welke volgorde worden getoond.
Deze scheiding is ook in overeenstemming met het Single Responsibility Principle. De Interactor is verantwoordelijk voor de business analist, de Presenter vertegenwoordigt de interactie-ontwerper, en de View is verantwoordelijk voor de visuele ontwerper.
Hieronder ziet u een diagram van de verschillende componenten en hoe ze zijn verbonden:
Hoewel de componenten van VIPER in elke volgorde in een toepassing kunnen worden geïmplementeerd, hebben we ervoor gekozen om de componenten te introduceren in de volgorde waarin we aanbevelen ze te implementeren. U zult merken dat deze volgorde ruwweg overeenkomt met het proces van het bouwen van een volledige applicatie, die begint met het bespreken van wat het product moet doen, gevolgd door hoe een gebruiker ermee zal interageren.
Interactor
Een Interactor vertegenwoordigt een enkele use case in de app. Het bevat de business logica om model objecten (Entiteiten) te manipuleren om een specifieke taak uit te voeren. Het werk dat in een Interactor wordt gedaan moet onafhankelijk zijn van enige UI. Dezelfde Interactor zou kunnen worden gebruikt in een iOS app of een OS X app.
Omdat de Interactor een PONSO (Plain Old NSObject
) is die voornamelijk logica bevat, is het eenvoudig te ontwikkelen met behulp van TDD.
De primaire use case voor de voorbeeld app is om de gebruiker alle komende to-do items te laten zien (d.w.z. alles wat eind volgende week klaar moet zijn). De business logica voor deze use case is om alle to-do items te vinden die tussen vandaag en eind volgende week moeten worden uitgevoerd en een relatieve vervaldatum toe te wijzen: vandaag, morgen, later deze week, of volgende week.
Hieronder staat de overeenkomstige methode uit VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Entities zijn de modelobjecten die door een Interactor worden gemanipuleerd. Entiteiten worden alleen door de Interactor gemanipuleerd. De Interactor geeft entiteiten nooit door aan de presentatielaag (d.w.z. Presenter).
Entities zijn meestal ook PONSO’s. Als u Core Data gebruikt, zult u willen dat uw beheerde objecten achter uw gegevenslaag blijven. Interactors zouden niet moeten werken met NSManagedObjects
.
Hier is de Entity voor ons to-do item:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Wees niet verbaasd als je entiteiten slechts gegevensstructuren zijn. Alle applicatie-afhankelijke logica zal waarschijnlijk in een Interactor zitten.
Presenter
De Presenter is een PONSO die voornamelijk bestaat uit logica om de UI aan te sturen. Het weet wanneer de gebruikersinterface moet worden gepresenteerd. Het verzamelt input van gebruikersinteracties, zodat het de UI kan updaten en verzoeken naar een Interactor kan sturen.
Wanneer de gebruiker op de + knop tikt om een nieuw to-do item toe te voegen, wordt addNewEntry
aangeroepen. Voor deze actie vraagt de Presenter het wireframe om de UI voor het toevoegen van een nieuw item te presenteren:
- (void)addNewEntry{ ;}
De Presenter ontvangt ook resultaten van een Interactor en converteert de resultaten in een vorm die efficiënt is om weer te geven in een View.
Hieronder staat de methode die aankomende items van de Interactor ontvangt. Het zal de gegevens verwerken en bepalen wat aan de gebruiker moet worden getoond:
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Entities worden nooit doorgegeven van de Interactor aan de Presenter. In plaats daarvan worden eenvoudige datastructuren zonder gedrag doorgegeven van de Interactor aan de Presenter. Dit voorkomt dat er ‘echt werk’ wordt gedaan in de Presenter. De Presenter kan alleen de gegevens voorbereiden voor weergave in de View.
View
De View is passief. Het wacht tot de Presenter het inhoud geeft om weer te geven; het vraagt de Presenter nooit om gegevens. Methoden die voor een View zijn gedefinieerd (b.v. LoginView voor een loginscherm) moeten een Presenter in staat stellen op een hoger abstractieniveau te communiceren, uitgedrukt in termen van de inhoud, en niet hoe die inhoud moet worden weergegeven. De Presenter weet niet van het bestaan van UILabel
, UIButton
, enz. De Presenter weet alleen welke inhoud hij onderhoudt en wanneer deze moet worden weergegeven. Het is aan de View om te bepalen hoe de inhoud wordt weergegeven.
De View is een abstracte interface, gedefinieerd in Objective-C met een protocol. Een UIViewController
of een van zijn subklassen zal het View-protocol implementeren. Bijvoorbeeld, het ‘voeg toe’ scherm uit ons voorbeeld heeft de volgende interface:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
Views en view controllers behandelen ook gebruikersinteractie en -invoer. Het is gemakkelijk te begrijpen waarom view controllers meestal zo groot worden, omdat zij de gemakkelijkste plaats zijn om deze input te verwerken om een of andere actie uit te voeren. Om onze view controllers slank te houden, moeten we ze een manier geven om belanghebbende partijen te informeren wanneer een gebruiker bepaalde acties onderneemt. De view controller moet geen beslissingen nemen op basis van deze acties, maar het moet deze gebeurtenissen doorgeven aan iets dat dat wel kan.
In ons voorbeeld heeft de Add View Controller een event handler eigenschap die voldoet aan de volgende interface:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
Wanneer de gebruiker op de cancel knop tikt, vertelt de view controller deze event handler dat de gebruiker heeft aangegeven dat het de add actie moet annuleren. Op die manier kan de event handler er voor zorgen dat de add view controller wordt ontslagen en de list view wordt verteld om te updaten.
De grens tussen de View en de Presenter is ook een geweldige plek voor ReactiveCocoa. In dit voorbeeld zou de view controller ook methoden kunnen bieden om signalen terug te sturen die knopacties vertegenwoordigen. Dit zou de Presenter in staat stellen om eenvoudig te reageren op die signalen zonder de scheiding van verantwoordelijkheden te doorbreken.
Routing
Routes van het ene scherm naar het andere worden gedefinieerd in de wireframes gemaakt door een interactie ontwerper. In VIPER wordt de verantwoordelijkheid voor Routing gedeeld door twee objecten: de Presenter, en het wireframe. Een wireframe-object is eigenaar van de UIWindow
, UINavigationController
, UIViewController
, enz. Het is verantwoordelijk voor het maken van een View/ViewController en het installeren daarvan in het venster.
Omdat de Presenter de logica bevat om te reageren op gebruikersinvoer, is het de Presenter die weet wanneer naar een ander scherm moet worden genavigeerd, en naar welk scherm moet worden genavigeerd. Ondertussen weet het wireframe hoe te navigeren. Dus zal de Presenter het wireframe gebruiken om de navigatie uit te voeren. Samen beschrijven ze een route van het ene scherm naar het volgende.
Het wireframe is ook een voor de hand liggende plaats om navigatie-overgangsanimaties te verwerken. Kijk eens naar dit voorbeeld van de add wireframe:
@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
De app gebruikt een aangepaste view controller overgang om de add view controller te presenteren. Aangezien het wireframe verantwoordelijk is voor het uitvoeren van de overgang, wordt het de overgangsdelegeerder voor de add view controller en kan het de juiste overgangsanimaties teruggeven.
Applicatie-componenten die passen in VIPER
In de architectuur van een iOS applicatie moet rekening worden gehouden met het feit dat UIKit en Cocoa Touch de belangrijkste tools zijn waar apps op worden gebouwd. De architectuur moet vreedzaam samengaan met alle componenten van de applicatie, maar het moet ook richtlijnen geven voor hoe sommige delen van de raamwerken worden gebruikt en waar ze wonen.
Het werkpaard van een iOS app is UIViewController
. Het zou gemakkelijk zijn om te veronderstellen dat een mededinger om MVC te vervangen zou terugdeinzen voor het maken van zwaar gebruik van view controllers. Maar view controllers staan centraal in het platform: ze verwerken oriëntatie veranderingen, reageren op input van de gebruiker, integreren goed met systeem componenten zoals navigatie controllers, en nu met iOS 7, maken ze aanpasbare overgangen tussen schermen mogelijk. Ze zijn uiterst nuttig.
Met VIPER doet een view controller precies waar hij voor bedoeld is: hij regelt de weergave. Onze to-do lijst app heeft twee view controllers, een voor het lijst scherm, en een voor het toevoeg scherm. De add view controller implementatie is extreem simpel omdat het alleen maar de view hoeft te besturen:
@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
Apps zijn meestal veel overtuigender als ze op het netwerk zijn aangesloten. Maar waar moet dit netwerken plaatsvinden en wat moet verantwoordelijk zijn voor het initiëren ervan? Het is typisch aan de Interactor om een netwerk operatie te starten, maar het zal de netwerk code niet direct afhandelen. Hij zal een afhankelijke partij vragen, zoals een netwerk manager of API client. Het kan zijn dat de Interactor gegevens uit meerdere bronnen moet samenvoegen om de informatie te leveren die nodig is om een use case te vervullen. Dan is het aan de Presentator om de gegevens die door de Interactor worden geretourneerd te nemen en deze te formatteren voor presentatie.
Een data store is verantwoordelijk voor het leveren van entiteiten aan een Interactor. Als een Interactor zijn business logica toepast, zal het entiteiten moeten ophalen uit de data store, de entiteiten manipuleren, en dan de bijgewerkte entiteiten terugzetten in de data store. De data store beheert de persistentie van de entiteiten. Entiteiten weten niets van de data store, dus entiteiten weten niet hoe ze zichzelf moeten persisteren.
De Interactor zou ook niet moeten weten hoe hij de entiteiten moet persisteren. Soms wil de Interactor een type object gebruiken dat een data manager heet, om zijn interactie met de data store te vergemakkelijken. De datamanager handelt meer van de winkel-specifieke operaties af, zoals het maken van fetch requests, het bouwen van queries, enz. Hierdoor kan de Interactor zich meer richten op de applicatielogica en hoeft hij niets te weten over hoe entiteiten worden verzameld of bewaard. Een voorbeeld van wanneer het zinvol is om een data manager te gebruiken is wanneer je Core Data gebruikt, wat hieronder wordt beschreven.
Hier is de interface voor de data manager van de voorbeeld app:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
Wanneer je TDD gebruikt om een Interactor te ontwikkelen, is het mogelijk om de productie data store te vervangen door een test double/mock. Door niet te praten met een externe server (voor een web service) of de schijf aan te raken (voor een database) kunnen je tests sneller en herhaalbaarder zijn.
Eén reden om de data store als een aparte laag te houden met duidelijke grenzen is dat het je in staat stelt om het kiezen van een specifieke persistentie technologie uit te stellen. Als je data store een enkele klasse is, kun je je app beginnen met een basis persistentie strategie, en dan later upgraden naar SQLite of Core Data als en wanneer het zinvol is om dat te doen, allemaal zonder iets anders te veranderen in de code base van je applicatie.
Het gebruik van Core Data in een iOS project kan vaak meer discussie oproepen dan de architectuur zelf. Echter, het gebruik van Core Data met VIPER kan de beste Core Data ervaring zijn die je ooit hebt gehad. Core Data is een geweldig hulpmiddel voor het bewaren van gegevens met behoud van snelle toegang en een laag geheugengebruik. Maar het heeft de gewoonte om zijn NSManagedObjectContext
ranken overal in de implementatiebestanden van een app te laten slingeren, vooral waar ze niet zouden moeten zijn. VIPER houdt Core Data waar het hoort te zijn: in de data store laag.
In het voorbeeld van de takenlijst zijn de enige twee delen van de app die weten dat Core Data wordt gebruikt, de data store zelf, die de Core Data stack opzet, en de data manager. De datamanager voert een fetch-verzoek uit, converteert de NSManagedObjects
die door de data store worden geretourneerd in standaard PONSO-modelobjecten, en geeft die terug aan de business logic layer. Op die manier is de kern van de applicatie nooit afhankelijk van Core Data, en als bonus hoeft u zich nooit zorgen te maken over verouderde of slecht geregen NSManagedObjects
die het werk in de war sturen.
Hier ziet het eruit binnen de datamanager wanneer een verzoek wordt gedaan om toegang te krijgen tot de Core Data store:
@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
Bijna net zo controversieel als Core Data zijn UI Storyboards. Storyboards hebben veel nuttige functies, en ze helemaal negeren zou een vergissing zijn. Het is echter moeilijk om alle doelen van VIPER te bereiken en tegelijkertijd alle functies te gebruiken die een storyboard te bieden heeft.
Het compromis dat we meestal maken is om geen segues te gebruiken. Er kunnen gevallen zijn waarin het gebruik van een segue zinvol is, maar het gevaar van segues is dat ze het erg moeilijk maken om de scheiding tussen schermen – en tussen UI en applicatielogica – intact te houden. Als vuistregel proberen we geen segues te gebruiken als het implementeren van de prepareForSegue methode noodzakelijk lijkt.
Overigens zijn storyboards een geweldige manier om de layout voor je gebruikersinterface te implementeren, vooral als je Auto Layout gebruikt. We kozen ervoor om beide schermen voor het to-do lijst voorbeeld te implementeren met behulp van een storyboard, en code zoals deze te gebruiken om onze eigen navigatie uit te voeren:
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
Viper gebruiken om modules te bouwen
Vaak wanneer je met VIPER werkt, zul je merken dat een scherm of een set van schermen de neiging heeft om samen te komen als een module. Een module kan op een paar manieren worden beschreven, maar meestal is het het beste om te denken aan een functie. In een podcasting app, kan een module de audio speler zijn of de abonnementen browser. In onze to-do lijst app, de lijst en voeg schermen zijn elk gebouwd als afzonderlijke modules.
Er zijn een paar voordelen aan het ontwerpen van uw app als een set van modules. Een daarvan is dat modules zeer duidelijke en goed gedefinieerde interfaces kunnen hebben, en ook onafhankelijk zijn van andere modules. Dit maakt het veel gemakkelijker om functies toe te voegen of te verwijderen, of om de manier te veranderen waarop je interface verschillende modules aan de gebruiker presenteert.
We wilden de scheiding tussen modules heel duidelijk maken in het to-do lijst voorbeeld, dus hebben we twee protocollen gedefinieerd voor de add module. De eerste is de module-interface, die definieert wat de module kan doen. De tweede is de module delegate, die beschrijft wat de module deed. Voorbeeld:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Omdat een module gepresenteerd moet worden om van veel waarde te zijn voor de gebruiker, implementeert de module’s Presenter gewoonlijk de module interface. Wanneer een andere module deze wil presenteren, zal zijn Presenter het module delegate protocol implementeren, zodat deze weet wat de module deed terwijl deze werd gepresenteerd.
Een module kan een gemeenschappelijke applicatielogica laag bevatten van entiteiten, interactors, en managers die voor meerdere schermen kunnen worden gebruikt. Dit hangt natuurlijk af van de interactie tussen deze schermen en hoe gelijksoortig ze zijn. Een module kan evengoed slechts één enkel scherm vertegenwoordigen, zoals in het voorbeeld van de takenlijst. In dit geval kan de applicatielogica laag zeer specifiek zijn voor het gedrag van zijn specifieke module.
Modules zijn ook gewoon een goede eenvoudige manier om code te organiseren. Door alle code voor een module in een eigen map en groep in Xcode te bewaren, is het eenvoudig te vinden wanneer je iets moet veranderen. Het is een geweldig gevoel wanneer je een class precies vindt waar je hem verwachtte te vinden.
Een ander voordeel van het bouwen van modules met VIPER is dat ze eenvoudiger uit te breiden zijn naar meerdere form factors. Als je de applicatielogica voor al je use-cases geïsoleerd hebt in de Interactor-laag, kun je je concentreren op het bouwen van de nieuwe gebruikersinterface voor tablet, telefoon of Mac, terwijl je je applicatielaag hergebruikt.
Dit nog een stap verder doortrekkend, kan de gebruikersinterface voor iPad-apps in staat zijn om sommige views, view controllers en presenters van de iPhone-app te hergebruiken. In dit geval zou een iPad scherm worden gerepresenteerd door ‘super’ presenters en wireframes, die het scherm zouden samenstellen met behulp van bestaande presenters en wireframes die zijn geschreven voor de iPhone. Het bouwen en onderhouden van een app op meerdere platforms kan een hele uitdaging zijn, maar een goede architectuur die hergebruik in de model- en applicatielaag bevordert, helpt dit veel gemakkelijker te maken.
Testen met VIPER
Het volgen van VIPER stimuleert een scheiding van aandachtspunten die het gemakkelijker maakt om TDD toe te passen. De Interactor bevat pure logica die onafhankelijk is van enige UI, wat het gemakkelijk maakt om met tests te rijden. De Presenter bevat logica om gegevens voor te bereiden voor weergave en is onafhankelijk van UIKit-widgets. Het ontwikkelen van deze logica is ook eenvoudig aan te sturen met tests.
Onze voorkeursmethode is om te beginnen met de Interactor. Alles in de UI is er om de behoeften van de use case te dienen. Door TDD te gebruiken om de API voor de Interactor te testen, krijg je een beter begrip van de relatie tussen de UI en de use case.
Als voorbeeld zullen we kijken naar de Interactor die verantwoordelijk is voor de lijst met aankomende to-do items. Het beleid voor het vinden van komende items is het vinden van alle to-do items te wijten aan het einde van de volgende week en classificeren van elke to-do item als zijnde verschuldigd vandaag, morgen, later deze week, of volgende week.
De eerste test die we schrijven is om er zeker van te zijn dat de Interactor alle taken vindt die eind volgende week klaar moeten zijn:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
Als we eenmaal weten dat de Interactor naar de juiste taken vraagt, zullen we verschillende tests schrijven om te bevestigen dat hij de taken aan de juiste relatieve datumgroep toewijst (b.v. vandaag, morgen, enz.).b.v. vandaag, morgen, etc.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Nu we weten hoe de API voor de Interactor eruit ziet, kunnen we de Presenter ontwikkelen. Wanneer de Presenter to-do items ontvangt van de Interactor, willen we testen of we de data goed formatteren en weergeven in de UI:
- (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 = ; ];}
We willen ook testen of de app de juiste actie start wanneer de gebruiker een nieuw to-do item wil toevoegen:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
We kunnen nu de View ontwikkelen. Wanneer er geen aankomende to-do items zijn, willen we een speciale boodschap tonen:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
Wanneer er wel aankomende to-do items zijn, willen we er zeker van zijn dat de tabel wordt getoond:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Het eerst bouwen van de Interactor past natuurlijk bij TDD. Als je eerst de Interactor ontwikkelt, gevolgd door de Presenter, bouw je eerst een reeks tests rond die lagen en leg je de basis voor de implementatie van die use cases. Je kunt snel itereren op die klassen, omdat je geen interactie met de UI hoeft te hebben om ze te testen. Dan, wanneer je de View gaat ontwikkelen, heb je een werkende en geteste logica en presentatie laag om er verbinding mee te maken. Tegen de tijd dat je klaar bent met het ontwikkelen van de View, zul je misschien merken dat de eerste keer dat je de app draait alles gewoon werkt, omdat al je tests je vertellen dat het zal werken.
Conclusie
We hopen dat je genoten hebt van deze introductie in VIPER. Velen van u zullen zich nu afvragen hoe nu verder te gaan. Als je je volgende app zou willen bouwen met VIPER, waar zou je dan beginnen?
Dit artikel en ons voorbeeld van een app met VIPER zijn zo specifiek en duidelijk als we ze konden maken. Onze to-do lijst app is vrij eenvoudig, maar het zou ook nauwkeurig moeten uitleggen hoe je een app bouwt met VIPER. In een echt project, zal hoe nauw je dit voorbeeld volgt afhangen van je eigen set van uitdagingen en beperkingen. Onze ervaring is dat elk van onze projecten de aanpak van het gebruik van VIPER een beetje heeft aangepast, maar ze hebben allemaal veel baat gehad bij het gebruik ervan als leidraad voor hun aanpak.
Er kunnen gevallen zijn waarin je om verschillende redenen wilt afwijken van het pad dat VIPER heeft uitgestippeld. Misschien ben je een wirwar van ‘bunny’ objecten tegengekomen, of zou je app baat hebben bij het gebruik van segues in Storyboards. Dat is OK. In deze gevallen, overweeg de geest van wat VIPER vertegenwoordigt bij het maken van je beslissing. In de kern is VIPER een architectuur gebaseerd op het Single Responsibility Principle. Als je problemen hebt, denk dan aan dit principe als je beslist hoe je verder gaat.
Je vraagt je misschien ook af of het mogelijk is om VIPER in je bestaande app te gebruiken. In dit scenario, overweeg dan om een nieuwe functie te bouwen met VIPER. Veel van onze bestaande projecten hebben deze route genomen. Dit stelt je in staat om een module te bouwen met VIPER, en helpt je ook om eventuele bestaande problemen op te sporen die het moeilijker zouden kunnen maken om een architectuur te adopteren die gebaseerd is op het Single Responsibility Principle.
Eén van de mooie dingen aan het ontwikkelen van software is dat elke app anders is, en er zijn ook verschillende manieren om een app te architectureren. Voor ons betekent dit dat elke app een nieuwe kans is om te leren en nieuwe dingen te proberen. Als je besluit om VIPER te proberen, denken we dat je ook een paar nieuwe dingen zult leren. Bedankt voor het lezen.
Swift Addendum
Laatstleden week tijdens WWDC introduceerde Apple de programmeertaal Swift als de toekomst van Cocoa en Cocoa Touch ontwikkeling. Het is nog te vroeg om een complexe mening te vormen over de Swift taal, maar we weten wel dat talen een grote invloed hebben op hoe we software ontwerpen en bouwen. We besloten om onze VIPER TODO voorbeeld app te herschrijven met Swift om ons te helpen leren wat dit betekent voor VIPER. Tot nu toe, bevalt het ons wat we zien. Hier zijn een paar functies van Swift waarvan we denken dat ze de ervaring van het bouwen van apps met VIPER zullen verbeteren.
Structs
In VIPER gebruiken we kleine, lichtgewicht, modelklassen om gegevens door te geven tussen lagen, zoals van de Presenter naar de View. Deze PONSO’s zijn meestal bedoeld om gewoon kleine hoeveelheden gegevens te dragen, en zijn meestal niet bedoeld om te worden ondergeclassificeerd. Swift structs zijn perfect geschikt voor deze situaties. Hier is een voorbeeld van een struct die gebruikt wordt in het VIPER Swift voorbeeld. Merk op dat deze struct gelijkwaardig moet zijn, en dus hebben we de == operator overloaded om twee instanties van zijn type te vergelijken:
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
Misschien wel het grootste verschil tussen Objective-C en Swift is hoe de twee omgaan met types. Objective-C is dynamisch getypeerd en Swift is zeer bewust strikt met hoe het type-controle implementeert tijdens het compileren. Voor een architectuur als VIPER, waar een app is opgebouwd uit meerdere verschillende lagen, kan type-veiligheid een enorme winst zijn voor de efficiëntie van de programmeur en voor de structuur van de architectuur. De compiler helpt je om er zeker van te zijn dat containers en objecten van het juiste type zijn wanneer ze worden doorgegeven tussen laaggrenzen. Dit is een goede plaats om structs te gebruiken, zoals hierboven getoond. Als een struct bedoeld is om op de grens tussen twee lagen te leven, dan kun je garanderen dat het nooit tussen die lagen uit kan komen, dankzij de type veiligheid.
Verder lezen
- VIPER TODO, artikel voorbeeld app
- VIPER SWIFT, artikel voorbeeld app gebouwd met Swift
- Counter, nog een voorbeeld app
- Mutual Mobile Inleiding tot VIPER
- Clean Architecture
- Lighter View Controllers
- Testing View Controllers
- Bunnies