Det är välkänt inom arkitekturen att vi formar våra byggnader och att våra byggnader formar oss. Som alla programmerare så småningom lär sig gäller detta lika bra för att bygga programvara.
Det är viktigt att utforma vår kod så att varje del är lätt att identifiera, har ett specifikt och uppenbart syfte och passar ihop med andra delar på ett logiskt sätt. Detta är vad vi kallar mjukvaruarkitektur. En bra arkitektur är inte det som gör en produkt framgångsrik, men den gör en produkt underhållbar och bidrar till att bevara förståndet hos de personer som underhåller den!
I den här artikeln kommer vi att presentera ett tillvägagångssätt för iOS-applikationsarkitektur som kallas VIPER. VIPER har använts för att bygga många stora projekt, men i den här artikeln kommer vi att visa dig VIPER genom att bygga en app med en att-göra-lista. Du kan följa med i exempelprojektet här på GitHub:
Vad är VIPER?
Testning har inte alltid varit en viktig del av byggandet av iOS-appar. När vi började sträva efter att förbättra våra testmetoder på Mutual Mobile upptäckte vi att det var svårt att skriva tester för iOS-appar. Vi bestämde oss för att om vi skulle förbättra sättet att testa vår programvara måste vi först komma på ett bättre sätt att bygga våra appar. Vi kallar den metoden VIPER.
VIPER är en tillämpning av Clean Architecture på iOS-appar. Ordet VIPER är en backronym för View, Interactor, Presenter, Entity och Routing. Clean Architecture delar upp en apps logiska struktur i olika ansvarslager. Detta gör det lättare att isolera beroenden (t.ex. din databas) och att testa interaktionerna vid gränserna mellan lagren:
De flesta iOS-appar är uppbyggda med MVC (model-view-controller). Att använda MVC som programarkitektur kan leda dig till att tänka att varje klass är antingen en modell, en vy eller en controller. Eftersom mycket av programlogiken inte hör hemma i en modell eller vy hamnar den oftast i kontrollern. Detta leder till ett problem som kallas Massive View Controller, där view controllern gör för mycket. Att banta ner dessa massiva view controllers är inte den enda utmaningen för iOS-utvecklare som vill förbättra kvaliteten på sin kod, men det är ett bra ställe att börja.
VIPER:s distinkta lager hjälper till att hantera denna utmaning genom att tillhandahålla tydliga platser för programlogik och navigeringsrelaterad kod. När VIPER har tillämpats kommer du att märka att vykontrollanterna i vårt exempel på en att-göra-lista är smala, effektiva, vykontrollerande maskiner. Du kommer också att märka att koden i view controllers och alla andra klasser är lätt att förstå, lättare att testa och därmed också lättare att underhålla.
Applikationsdesign baserad på användningsfall
Applikationer implementeras ofta som en uppsättning användningsfall. Användningsfall kallas också acceptanskriterier eller beteenden och beskriver vad en app ska göra. En lista kanske måste kunna sorteras efter datum, typ eller namn. Det är ett användningsfall. Ett användningsfall är det lager i en applikation som ansvarar för affärslogiken. Användningsfallen bör vara oberoende av användargränssnittets genomförande av dem. De bör också vara små och väldefinierade. Att bestämma hur man ska dela upp en komplex applikation i mindre användningsfall är en utmaning och kräver övning, men det är ett användbart sätt att begränsa omfattningen av varje problem som du löser och varje klass som du skriver.
Byggandet av en applikation med VIPER innebär att man implementerar en uppsättning komponenter för att uppfylla varje användningsfall. Applikationslogik är en viktig del av genomförandet av ett användningsfall, men det är inte den enda delen. Användningsfallet påverkar också användargränssnittet. Dessutom är det viktigt att tänka på hur användningsfallet passar ihop med andra kärnkomponenter i en applikation, t.ex. nätverk och dataperspektiv. Komponenter fungerar som plugins för användningsfallen, och VIPER är ett sätt att beskriva vilken roll var och en av dessa komponenter har och hur de kan interagera med varandra.
Ett av användningsfallen eller kraven för vår app med en att-göra-lista var att gruppera de att-göra-uppgifter på olika sätt baserat på användarens val. Genom att separera logiken som organiserar dessa data i ett användningsfall kan vi hålla koden för användargränssnittet ren och enkelt svepa in användningsfallet i tester för att se till att det fortsätter att fungera på det sätt som vi förväntar oss.
Huvuddelar i VIPER
Huvuddelarna i VIPER är:
- View: Visar det som presentatören säger åt den att göra och vidarebefordrar användarinmatning tillbaka till presentatören.
- Interactor: Innehåller den affärslogik som specificeras av ett användningsfall.
- Presenter: innehåller visningslogik för att förbereda innehåll för visning (som tas emot från Interactor) och för att reagera på användarinmatningar (genom att begära nya data från Interactor).
- Entity: innehåller grundläggande modellobjekt som används av Interactor.
- Routing: innehåller navigeringslogik för att beskriva vilka skärmar som visas i vilken ordning.
Denna uppdelning är också i överensstämmelse med principen om ett enda ansvar. Interactor är ansvarig för affärsanalytikern, Presenter representerar interaktionsdesignern och View är ansvarig för den visuella designern.
Nedan följer ett diagram över de olika komponenterna och hur de är sammankopplade:
Men även om komponenterna i VIPER kan implementeras i en applikation i vilken ordning som helst, har vi valt att presentera komponenterna i den ordning som vi rekommenderar att de implementeras. Du kommer att märka att denna ordning stämmer ungefär överens med processen för att bygga en hel applikation, som börjar med att diskutera vad produkten behöver göra, följt av hur en användare kommer att interagera med den.
Interactor
En Interactor representerar ett enskilt användningsfall i applikationen. Den innehåller affärslogiken för att manipulera modellobjekt (entiteter) för att utföra en specifik uppgift. Det arbete som utförs i en interaktör ska vara oberoende av något användargränssnitt. Samma Interactor kan användas i en iOS-app eller en OS X-app.
Då Interactor är en PONSO (Plain Old NSObject
) som i första hand innehåller logik, är den lätt att utveckla med hjälp av TDD.
Det primära användningsfallet för exempelappen är att visa användaren alla kommande att-göra-objekt (dvs. allt som ska vara klart i slutet av nästa vecka). Affärslogiken för det här användningsfallet är att hitta alla uppgifter som ska betalas mellan idag och slutet av nästa vecka och tilldela ett relativt förfallodatum: idag, imorgon, senare den här veckan eller nästa vecka.
Nedan följer motsvarande metod från VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Entiteter är de modellobjekt som manipuleras av en Interactor. Enheter manipuleras endast av interaktören. Interaktören skickar aldrig enheter till presentationsskiktet (dvs. presentatör).
Entiteter tenderar också att vara PONSOs. Om du använder Core Data vill du att dina hanterade objekt ska ligga bakom datalaget. Interaktörer bör inte arbeta med NSManagedObjects
.
Här är entiteten för vårt ärende:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Var inte förvånad om dina entiteter bara är datastrukturer. All programberoende logik kommer sannolikt att finnas i en Interactor.
Presenter
Presenter är en PONSO som huvudsakligen består av logik för att driva användargränssnittet. Den vet när användargränssnittet ska presenteras. Den samlar in data från användarinteraktioner så att den kan uppdatera användargränssnittet och skicka förfrågningar till en interaktör.
När användaren trycker på +-knappen för att lägga till ett nytt ärende, anropas addNewEntry
. För denna åtgärd ber presentatören wireframe att presentera användargränssnittet för att lägga till ett nytt objekt:
- (void)addNewEntry{ ;}
Presentatören tar också emot resultat från en Interactor och omvandlar resultaten till ett formulär som är effektivt att visa i en vy.
Nedanför finns metoden som tar emot kommande objekt från Interactor. Den bearbetar data och bestämmer vad som ska visas för användaren:
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Enheter överförs aldrig från Interactor till Presenter. Istället skickas enkla datastrukturer som inte har något beteende från Interactor till Presenter. Detta förhindrar att något ”riktigt arbete” utförs i presentatorn. Presenteraren kan bara förbereda data för visning i vyn.
View
Vyn är passiv. Den väntar på att presentatören ska ge den innehåll att visa; den ber aldrig presentatören om data. Metoder som definieras för en vy (t.ex. LoginView för en inloggningsskärm) bör göra det möjligt för en presentatör att kommunicera på en högre abstraktionsnivå, uttryckt i termer av dess innehåll och inte hur detta innehåll ska visas. Presentatören känner inte till existensen av UILabel
, UIButton
osv. Presenteraren vet bara om det innehåll som den upprätthåller och när det ska visas. Det är upp till vyn att bestämma hur innehållet ska visas.
Vyn är ett abstrakt gränssnitt som definieras i Objective-C med ett protokoll. En UIViewController
eller en av dess underklasser implementerar View-protokollet. Till exempel har skärmen ”add” från vårt exempel följande gränssnitt:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
Vyer och view controllers hanterar också användarinteraktion och inmatning. Det är lätt att förstå varför view controllers vanligtvis blir så stora, eftersom de är det enklaste stället att hantera denna input för att utföra någon åtgärd. För att hålla våra view controllers smala måste vi ge dem ett sätt att informera berörda parter när en användare utför vissa åtgärder. Vykontrollanten bör inte fatta beslut baserat på dessa handlingar, men den bör vidarebefordra dessa händelser till något som kan göra det.
I vårt exempel har Add View Controller en egenskap för händelsehanterare som överensstämmer med följande gränssnitt:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
När användaren trycker på avbrytningsknappen talar vykontrollanten om för den här händelsehanteraren att användaren har angett att den ska avbryta add-åtgärden. På så sätt kan händelsehanteraren ta hand om att avfärda visningskontrollanten för tillägg och tala om för listvyn att den ska uppdateras.
Gränsen mellan vyn och presentatören är också en utmärkt plats för ReactiveCocoa. I det här exemplet kan visningskontrollanten också tillhandahålla metoder för att returnera signaler som representerar knappåtgärder. Detta skulle göra det möjligt för Presenter att enkelt reagera på dessa signaler utan att bryta ansvarsfördelningen.
Routing
Rutter från en skärm till en annan definieras i de wireframes som skapas av en interaktionsdesigner. I VIPER delas ansvaret för Routing mellan två objekt: Presenteraren och trådramen. Ett wireframe-objekt äger UIWindow
, UINavigationController
, UIViewController
osv. Det är ansvarigt för att skapa en View/ViewController och installera den i fönstret.
Då Presenteraren innehåller logiken för att reagera på användarinmatningar är det Presenteraren som vet när man ska navigera till en annan skärm och vilken skärm man ska navigera till. Under tiden vet wireframe hur man ska navigera. Presenteraren kommer därför att använda trådramen för att utföra navigeringen. Tillsammans beskriver de en väg från en skärm till nästa.
Trådramen är också en självklar plats för att hantera animationer för navigationsövergångar. Ta en titt på det här exemplet från wireframe för add:
@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
Appen använder en anpassad vykontrollantövergång för att presentera vykontrollanten för add. Eftersom wireframe är ansvarig för att utföra övergången blir den övergångsdelegaten för add view controller och kan returnera lämpliga övergångsanimationer.
Applikationskomponenter som passar in i VIPER
En iOS-applikationsarkitektur måste ta hänsyn till att UIKit och Cocoa Touch är de viktigaste verktygen som appar byggs ovanpå. Arkitekturen måste samexistera fredligt med alla komponenter i applikationen, men den måste också ge riktlinjer för hur vissa delar av ramverken används och var de bor.
Arbetshästen i en iOS-app är UIViewController
. Det skulle vara lätt att anta att en utmanare som ska ersätta MVC skulle skygga för att göra stor användning av view controllers. Men view controllers är centrala för plattformen: de hanterar orienteringsförändringar, reagerar på input från användaren, integreras väl med systemkomponenter som navigationscontrollers och med iOS 7 möjliggör de anpassningsbara övergångar mellan skärmar. De är extremt användbara.
Med VIPER gör en vykontroller exakt vad den var tänkt att göra: den kontrollerar vyn. Vår app för att göra-lista har två view controllers, en för listskärmen och en för tilläggsskärmen. Implementationen av visningskontrollanten för add är extremt enkel eftersom allt den behöver göra är att kontrollera vyn:
@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
Appar är vanligtvis mycket mer övertygande när de är anslutna till nätverket. Men var ska detta nätverk ske och vad ska ansvara för att initiera det? Det är vanligtvis upp till Interactor att initiera en nätverksoperation, men den kommer inte att hantera nätverkskoden direkt. Den kommer att fråga ett beroende, t.ex. en nätverkshanterare eller API-klient. Interaktören kan behöva samla data från flera källor för att tillhandahålla den information som behövs för att uppfylla ett användningsfall. Sedan är det upp till presentatören att ta de data som Interactor returnerar och formatera dem för presentation.
Ett datalager ansvarar för att tillhandahålla enheter till en Interactor. När en interaktör tillämpar sin affärslogik måste den hämta enheter från datalagret, manipulera enheterna och sedan lägga tillbaka de uppdaterade enheterna i datalagret. Datalagret hanterar persistensen av enheterna. Enheterna känner inte till datalagret, så enheterna vet inte hur de ska upprätthålla sig själva.
Interaktören bör inte heller veta hur man upprätthåller entiteterna. Ibland kan Interactor vilja använda en typ av objekt som kallas datahanterare för att underlätta interaktionen med datalagret. Datahanteraren hanterar fler av de butiksspecifika typerna av operationer, som att skapa hämtningsförfrågningar, bygga upp förfrågningar osv. Detta gör att Interactor kan fokusera mer på programlogik och inte behöver veta något om hur enheter samlas in eller lagras. Ett exempel på när det är meningsfullt att använda en datahanterare är när du använder Core Data, som beskrivs nedan.
Här är gränssnittet för exempelappens datahanterare:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
När man använder TDD för att utveckla en Interactor är det möjligt att byta ut produktionsdatalagret mot en testdubbel/mock. Att inte prata med en fjärrserver (för en webbtjänst) eller röra disken (för en databas) gör att dina tester blir snabbare och mer repeterbara.
En anledning till att behålla datalagret som ett distinkt lager med tydliga gränser är att det gör att du kan vänta med att välja en specifik persistensteknik. Om datalagret är en enda klass kan du starta din app med en grundläggande persistensstrategi och sedan uppgradera till SQLite eller Core Data senare om och när det är vettigt, allt utan att ändra något annat i applikationens kodbas.
Användning av Core Data i ett iOS-projekt kan ofta ge upphov till mer debatt än själva arkitekturen. Att använda Core Data med VIPER kan dock bli den bästa Core Data-upplevelsen du någonsin haft. Core Data är ett utmärkt verktyg för att bevara data samtidigt som man behåller snabb åtkomst och ett lågt minnesutrymme. Men det har en vana att slingra sina NSManagedObjectContext
rännor genom hela appens implementeringsfiler, särskilt där de inte ska vara. VIPER håller Core Data där det ska vara: i datalagringslagret.
I exemplet med att göra-listan är de enda två delarna av appen som vet att Core Data används datalageret självt, som ställer in Core Data-stacken, och datahanteraren. Datahanteraren utför en hämtningsförfrågan, omvandlar NSManagedObjects
som returneras av datalagret till standard PONSO-modellobjekt och skickar dessa tillbaka till affärslogiklagret. På så sätt är kärnan i applikationen aldrig beroende av Core Data, och som en bonus behöver du aldrig oroa dig för att föråldrade eller dåligt trådade NSManagedObjects
ska ställa till det.
Så här ser det ut inne i datahanteraren när en förfrågan görs för att få tillgång till Core Data-lagret:
@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
Nästan lika kontroversiellt som Core Data är UI Storyboards. Storyboards har många användbara funktioner och att helt ignorera dem vore ett misstag. Det är dock svårt att uppnå alla VIPER:s mål samtidigt som man använder alla funktioner som en storyboard har att erbjuda.
Den kompromiss vi brukar göra är att välja att inte använda segues. Det kan finnas vissa fall där det är vettigt att använda segue, men faran med segues är att de gör det mycket svårt att hålla separationen mellan skärmar – liksom mellan UI och applikationslogik – intakt. Som tumregel försöker vi att inte använda segues om det verkar nödvändigt att implementera metoden prepareForSegue.
I övrigt är storyboards ett utmärkt sätt att implementera layouten för ditt användargränssnitt, särskilt när du använder Auto Layout. Vi valde att implementera båda skärmarna i exemplet med att göra-lista med hjälp av en storyboard och använda kod som denna för att utföra vår egen navigering:
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
Användning av VIPER för att bygga moduler
Ofta när du arbetar med VIPER kommer du att märka att en skärm eller en uppsättning skärmar tenderar att samlas som en modul. En modul kan beskrivas på flera olika sätt, men vanligtvis är det bäst att tänka på den som en funktion. I en podcast-app kan en modul vara ljudspelaren eller prenumerationswebbläsaren. I vår app med en att-göra-lista är skärmarna lista och lägga till byggda som separata moduler.
Det finns några fördelar med att utforma din app som en uppsättning moduler. En är att moduler kan ha mycket tydliga och väldefinierade gränssnitt samt vara oberoende av andra moduler. Detta gör det mycket lättare att lägga till/ta bort funktioner, eller att ändra hur ditt gränssnitt presenterar olika moduler för användaren.
Vi ville göra separationen mellan modulerna mycket tydlig i exemplet med att göra-listan, så vi definierade två protokoll för add-modulen. Det första är modulgränssnittet, som definierar vad modulen kan göra. Det andra är moduldelegaten, som beskriver vad modulen gjorde. Exempel:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Då en modul måste presenteras för att vara av stort värde för användaren, implementerar modulens Presenter vanligtvis modulens gränssnitt. När en annan modul vill presentera denna modul kommer dess Presenter att implementera moduldelegatprotokollet, så att den vet vad modulen gjorde medan den presenterades.
En modul kan innehålla ett gemensamt tillämpningslogiklager av enheter, interaktörer och förvaltare som kan användas för flera skärmar. Detta beror naturligtvis på interaktionen mellan dessa skärmar och hur lika de är. En modul kan lika gärna representera endast en enda skärm, vilket visas i exemplet med att göra-listan. I det här fallet kan applikationslogiklagret vara mycket specifikt för beteendet hos sin speciella modul.
Moduler är också bara ett bra enkelt sätt att organisera kod. Genom att hålla all kod för en modul undan i en egen mapp och grupp i Xcode är det lätt att hitta den när du behöver ändra något. Det är en fantastisk känsla när man hittar en klass precis där man förväntade sig att leta efter den.
En annan fördel med att bygga moduler med VIPER är att de blir lättare att utöka till flera olika formfaktorer. När programlogiken för alla dina användningsfall är isolerad i Interactor-lagret kan du fokusera på att bygga det nya användargränssnittet för surfplatta, telefon eller Mac, samtidigt som du återanvänder ditt applikationslager.
Om vi tar det här ett steg längre kan användargränssnittet för iPad-appar kanske återanvända en del av vyer, view controllers och presenters från iPhone-appen. I det här fallet skulle en iPad-skärm representeras av ”super” presenters och wireframes, som skulle komponera skärmen med hjälp av befintliga presenters och wireframes som skrevs för iPhone. Att bygga och underhålla en app på flera olika plattformar kan vara en stor utmaning, men en bra arkitektur som främjar återanvändning i modell- och applikationsskiktet underlättar detta avsevärt.
Testning med VIPER
Att följa VIPER uppmuntrar till en åtskillnad mellan olika problemområden som gör det lättare att införa TDD. Interactor innehåller ren logik som är oberoende av alla användargränssnitt, vilket gör det lätt att köra med tester. Presenter innehåller logik för att förbereda data för visning och är oberoende av alla UIKit-widgetar. Att utveckla denna logik är också lätt att driva med tester.
Vår föredragna metod är att börja med Interactor. Allt i användargränssnittet finns där för att tillgodose användningsfallets behov. Genom att använda TDD för att testa API:et för Interactor får du en bättre förståelse för förhållandet mellan användargränssnittet och användningsfallet.
Som exempel kommer vi att titta på den Interactor som ansvarar för listan över kommande to-do-objekt. Principen för att hitta kommande objekt är att hitta alla objekt som ska vara klara i slutet av nästa vecka och klassificera varje objekt som att det ska vara klart idag, imorgon, senare den här veckan eller nästa vecka.
Det första testet vi skriver är att se till att Interactor hittar alla uppgifter som ska vara klara i slutet av nästa vecka:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
När vi vet att Interactor frågar efter lämpliga uppgifter kommer vi att skriva flera tester för att bekräfta att den tilldelar uppgifterna till den rätta relativa datumgruppen (t.ex.t.ex. idag, imorgon osv.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
När vi nu vet hur API:et för Interactor ser ut kan vi utveckla Presenter. När Presenter tar emot kommande att-göra-objekt från Interactor vill vi testa att vi formaterar data korrekt och visar dem i 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 = ; ];}
Vi vill också testa att appen startar lämplig åtgärd när användaren vill lägga till ett nytt att-göra-objekt:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
Vi kan nu utveckla View. När det inte finns några kommande att-göra-objekt vill vi visa ett särskilt meddelande:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
När det finns kommande att-göra-objekt att visa vill vi se till att tabellen visas:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Att bygga Interactor först är en naturlig anpassning till TDD. Om du utvecklar Interactor först, följt av Presenter, får du möjlighet att bygga upp en uppsättning tester runt dessa lager först och lägga grunden för att implementera dessa användningsfall. Du kan snabbt iterera dessa klasser eftersom du inte behöver interagera med användargränssnittet för att testa dem. När du sedan utvecklar vyn har du en fungerande och testad logik och ett testat presentationslager att ansluta till den. När du är klar med att utveckla vyn kanske du upptäcker att när du kör appen första gången fungerar allting, eftersom alla dina godkända tester säger att det kommer att fungera.
Slutsats
Vi hoppas att du har haft glädje av den här introduktionen till VIPER. Många av er kanske nu undrar vad ni ska göra härnäst. Om du vill bygga din nästa app med hjälp av VIPER, var skulle du börja?
Denna artikel och vår exempelimplementering av en app med hjälp av VIPER är så specifika och väldefinierade som vi kunde göra dem. Vår app för att göra-lista är ganska okomplicerad, men den borde också förklara exakt hur man bygger en app med VIPER. I ett verkligt projekt kommer hur nära du följer detta exempel att bero på dina egna utmaningar och begränsningar. Vår erfarenhet är att alla våra projekt har varierat något när det gäller tillvägagångssättet för att använda VIPER, men alla har haft stor nytta av att använda det som vägledning.
Det kan finnas fall där du av olika anledningar vill avvika från den väg som VIPER stakat ut. Kanske har du stött på ett virrvarr av ”kaninobjekt”, eller så skulle din app kunna dra nytta av att använda segregeringar i Storyboards. Det är okej. I dessa fall bör du tänka på vad VIPER representerar när du fattar ditt beslut. VIPER är i grund och botten en arkitektur som bygger på principen om ett enda ansvar. Om du har problem, tänk på denna princip när du bestämmer dig för hur du ska gå vidare.
Du kanske också undrar om det är möjligt att använda VIPER i din befintliga app. I det här scenariot kan du överväga att bygga en ny funktion med VIPER. Många av våra befintliga projekt har valt denna väg. På så sätt kan du bygga en modul med hjälp av VIPER, och det hjälper dig också att upptäcka eventuella befintliga problem som kan göra det svårare att anta en arkitektur som bygger på principen om ett enda ansvar.
En av de fantastiska sakerna med att utveckla programvara är att varje app är annorlunda, och det finns också olika sätt att bygga upp arkitekturen för varje app. För oss innebär detta att varje app är en ny möjlighet att lära sig och prova nya saker. Om du bestämmer dig för att prova VIPER tror vi att du också kommer att lära dig några nya saker. Tack för att du läste.
Swift Addendum
Förra veckan på WWDC presenterade Apple programmeringsspråket Swift som framtiden för Cocoa- och Cocoa Touch-utveckling. Det är för tidigt att ha bildat komplexa åsikter om Swift-språket, men vi vet att språk har ett stort inflytande på hur vi designar och bygger programvara. Vi bestämde oss för att skriva om vår VIPER TODO-exempelapp med Swift för att lära oss vad detta innebär för VIPER. Hittills gillar vi vad vi ser. Här är några funktioner i Swift som vi tror kommer att förbättra upplevelsen av att bygga appar med VIPER.
Structs
I VIPER använder vi små, lättviktiga modellklasser för att överföra data mellan lager, till exempel från Presenter till View. Dessa PONSO-klasser är vanligtvis avsedda att helt enkelt överföra små mängder data och är vanligtvis inte avsedda att underklassas. Swift structs passar perfekt för dessa situationer. Här är ett exempel på en struct som används i Swift-exemplet VIPER. Lägg märke till att denna struct måste vara likvärdig, och därför har vi överbelastat ==-operatören för att jämföra två instanser av dess typ:
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
Den kanske största skillnaden mellan Objective-C och Swift är hur de två hanterar typer. Objective-C är dynamiskt typad och Swift är mycket avsiktligt strikt när det gäller hur det implementerar typkontroll vid kompilering. För en arkitektur som VIPER, där en app består av flera olika lager, kan typsäkerhet vara en stor vinst för programmerarnas effektivitet och för den arkitektoniska strukturen. Kompilatorn hjälper dig att se till att behållare och objekt är av rätt typ när de skickas mellan lagergränserna. Detta är ett utmärkt ställe att använda structs på det sätt som visas ovan. Om en struct är tänkt att leva vid gränsen mellan två lager kan du garantera att den aldrig kommer att kunna rymma mellan dessa lager tack vare typsäkerheten.
Fortsatt läsning
- VIPER TODO, artikel exempelapp
- VIPER SWIFT, artikel exempelapp byggd med Swift
- Counter, ytterligare en exempelapp
- Mutual Mobile Introduktion till VIPER
- Snygg arkitektur
- Lättare View Controllers
- Testing View Controllers
- Bunnies