È ben noto nel campo dell’architettura che noi diamo forma ai nostri edifici, e dopo i nostri edifici danno forma a noi. Come tutti i programmatori alla fine imparano, questo si applica altrettanto bene alla costruzione del software.
È importante progettare il nostro codice in modo che ogni pezzo sia facilmente identificabile, abbia uno scopo specifico e ovvio, e si adatti agli altri pezzi in modo logico. Questo è ciò che chiamiamo architettura del software. Una buona architettura non è ciò che rende un prodotto di successo, ma rende un prodotto mantenibile e aiuta a preservare la sanità mentale delle persone che lo mantengono!
In questo articolo, introdurremo un approccio all’architettura delle applicazioni iOS chiamato VIPER. VIPER è stato usato per costruire molti progetti di grandi dimensioni, ma per gli scopi di questo articolo vi mostreremo VIPER costruendo un’app per la lista delle cose da fare. Potete seguire il progetto di esempio qui su GitHub:
Che cos’è VIPER?
Il test non è sempre stato una parte importante nella costruzione di applicazioni iOS. Quando ci siamo imbarcati in una ricerca per migliorare le nostre pratiche di test a Mutual Mobile, abbiamo scoperto che scrivere test per le app iOS era difficile. Abbiamo deciso che se volevamo migliorare il modo in cui testiamo il nostro software, avremmo dovuto prima trovare un modo migliore per architettare le nostre applicazioni. Abbiamo chiamato questo metodo VIPER.
VIPER è un’applicazione della Clean Architecture alle applicazioni iOS. La parola VIPER è un backronimo per View, Interactor, Presenter, Entity e Routing. La Clean Architecture divide la struttura logica di un’app in livelli distinti di responsabilità. Questo rende più facile isolare le dipendenze (ad esempio il database) e testare le interazioni ai confini tra i livelli:
La maggior parte delle applicazioni iOS sono progettate utilizzando MVC (model-view-controller). Usare MVC come architettura dell’applicazione può portarvi a pensare che ogni classe sia un modello, una vista o un controller. Poiché molta della logica dell’applicazione non appartiene ad un modello o ad una vista, di solito finisce nel controllore. Questo porta ad un problema conosciuto come Massive View Controller, dove i view controller finiscono per fare troppo. Dimagrire questi massicci view controller non è l’unica sfida affrontata dagli sviluppatori iOS che cercano di migliorare la qualità del loro codice, ma è un ottimo punto di partenza.
I livelli distinti di VIPER aiutano ad affrontare questa sfida, fornendo posizioni chiare per la logica dell’applicazione e il codice relativo alla navigazione. Con VIPER applicato, noterete che i controllori di vista nel nostro esempio di lista di cose da fare sono macchine di controllo della vista snelle e medie. Troverete anche che il codice nei controller di visualizzazione e in tutte le altre classi è facile da capire, più facile da testare e, di conseguenza, anche più facile da mantenere.
Progettazione di applicazioni basate su casi d’uso
Le applicazioni sono spesso implementate come un insieme di casi d’uso. I casi d’uso sono anche conosciuti come criteri di accettazione, o comportamenti, e descrivono ciò che un’applicazione deve fare. Forse una lista deve essere ordinabile per data, tipo o nome. Questo è un caso d’uso. Un caso d’uso è il livello di un’applicazione che è responsabile della logica di business. I casi d’uso dovrebbero essere indipendenti dall’implementazione dell’interfaccia utente. Dovrebbero anche essere piccoli e ben definiti. Decidere come suddividere un’applicazione complessa in casi d’uso più piccoli è impegnativo e richiede pratica, ma è un modo utile per limitare la portata di ogni problema che stai risolvendo e di ogni classe che stai scrivendo.
Costruire un’applicazione con VIPER comporta l’implementazione di un insieme di componenti per soddisfare ogni caso d’uso. La logica dell’applicazione è una parte importante dell’implementazione di un caso d’uso, ma non è l’unica parte. Il caso d’uso influenza anche l’interfaccia utente. Inoltre, è importante considerare come il caso d’uso si adatta ad altri componenti fondamentali di un’applicazione, come la rete e la persistenza dei dati. I componenti agiscono come plugin per i casi d’uso, e VIPER è un modo di descrivere qual è il ruolo di ciascuno di questi componenti e come possono interagire l’uno con l’altro.
Uno dei casi d’uso o dei requisiti per la nostra app per la lista delle cose da fare era di raggruppare le cose da fare in modi diversi in base alla selezione dell’utente. Separando la logica che organizza i dati in un caso d’uso, siamo in grado di mantenere pulito il codice dell’interfaccia utente e di avvolgere facilmente il caso d’uso nei test per assicurarci che continui a funzionare come ci aspettiamo.
Parti principali di VIPER
Le parti principali di VIPER sono:
- View: visualizza ciò che gli viene detto dal Presenter e riporta l’input dell’utente al Presenter.
- Interactor: contiene la logica di business come specificato da un caso d’uso.
- Presenter: contiene la logica di visualizzazione per preparare il contenuto per la visualizzazione (come ricevuto dall’Interactor) e per reagire agli input dell’utente (richiedendo nuovi dati all’Interactor).
- Entity: contiene gli oggetti base del modello usati dall’Interactor.
- Routing: contiene la logica di navigazione per descrivere quali schermate sono mostrate in quale ordine.
Questa separazione è anche conforme al principio di responsabilità unica. L’Interactor è responsabile per il business analyst, il Presenter rappresenta l’interaction designer, e la View è responsabile per il visual designer.
Di seguito un diagramma dei diversi componenti e come sono collegati:
Sebbene i componenti di VIPER possano essere implementati in un’applicazione in qualsiasi ordine, abbiamo scelto di introdurre i componenti nell’ordine in cui consigliamo di implementarli. Noterete che questo ordine è approssimativamente coerente con il processo di costruzione di un’intera applicazione, che inizia con la discussione di ciò che il prodotto deve fare, seguito da come un utente interagirà con esso.
Interactor
Un Interactor rappresenta un singolo caso d’uso nell’applicazione. Contiene la logica di business per manipolare gli oggetti del modello (Entità) per eseguire un compito specifico. Il lavoro svolto in un Interactor dovrebbe essere indipendente da qualsiasi UI. Lo stesso Interactor potrebbe essere usato in un’app iOS o in un’app OS X.
Perché l’Interactor è un PONSO (Plain Old NSObject
) che contiene principalmente la logica, è facile da sviluppare usando TDD.
Il caso d’uso primario per l’app di esempio è quello di mostrare all’utente tutti i prossimi oggetti da fare (cioè qualsiasi cosa da fare entro la fine della prossima settimana). La logica di business per questo caso d’uso è di trovare tutti gli oggetti da fare tra oggi e la fine della prossima settimana e assegnare una data di scadenza relativa: oggi, domani, più tardi questa settimana, o la prossima settimana.
Di seguito il metodo corrispondente da VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Le entità sono gli oggetti del modello manipolati da un Interactor. Le entità sono manipolate solo dall’Interactor. L’Interactor non passa mai le entità al livello di presentazione (cioè Presenter).
Le entità tendono anche ad essere PONSO. Se stai usando Core Data, vorrai che i tuoi oggetti gestiti rimangano dietro il tuo strato di dati. Gli interattori non dovrebbero lavorare con NSManagedObjects
.
Ecco l’entità per il nostro oggetto da fare:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Non siate sorpresi se le vostre entità sono solo strutture di dati. Qualsiasi logica dipendente dall’applicazione sarà molto probabilmente in un Interactor.
Presenter
Il Presenter è un PONSO che consiste principalmente nella logica per guidare l’UI. Sa quando presentare l’interfaccia utente. Raccoglie input dalle interazioni dell’utente in modo da poter aggiornare l’UI e inviare richieste a un Interactor.
Quando l’utente tocca il pulsante + per aggiungere un nuovo elemento da fare, viene chiamato addNewEntry
. Per questa azione, il Presenter chiede al wireframe di presentare l’UI per l’aggiunta di un nuovo elemento:
- (void)addNewEntry{ ;}
Il Presenter riceve anche i risultati da un Interactor e converte i risultati in un modulo che è efficiente da visualizzare in una View.
Di seguito è il metodo che riceve gli elementi in arrivo dall’Interactor. Elaborerà i dati e determinerà cosa mostrare all’utente:
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Le entità non vengono mai passate dall’Interactor al Presenter. Invece, semplici strutture di dati che non hanno comportamento vengono passate dall’Interactor al Presenter. Questo impedisce che qualsiasi “lavoro reale” venga fatto nel Presenter. Il Presenter può solo preparare i dati per la visualizzazione nella View.
View
La View è passiva. Aspetta che il presentatore gli dia il contenuto da visualizzare; non chiede mai al presentatore i dati. I metodi definiti per una View (ad esempio LoginView per una schermata di login) dovrebbero consentire al Presentatore di comunicare ad un livello di astrazione superiore, espresso in termini di contenuto, e non di come tale contenuto debba essere visualizzato. Il presentatore non conosce l’esistenza di UILabel
, UIButton
, ecc. Il presentatore conosce solo il contenuto che mantiene e quando deve essere visualizzato. Spetta alla View determinare come il contenuto viene visualizzato.
La View è un’interfaccia astratta, definita in Objective-C con un protocollo. Una UIViewController
o una delle sue sottoclassi implementerà il protocollo View. Per esempio, la schermata ‘add’ del nostro esempio ha la seguente interfaccia:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
Le View e i view controller gestiscono anche l’interazione e l’input dell’utente. È facile capire perché i view controller di solito diventano così grandi, dato che sono il posto più semplice per gestire questo input per eseguire qualche azione. Per mantenere i nostri view controller snelli, dobbiamo dare loro un modo per informare le parti interessate quando un utente compie determinate azioni. Il view controller non dovrebbe prendere decisioni basate su queste azioni, ma dovrebbe passare questi eventi a qualcosa che può farlo.
Nel nostro esempio, Add View Controller ha una proprietà event handler che è conforme alla seguente interfaccia:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
Quando l’utente tocca il pulsante di cancellazione, il view controller dice a questo event handler che l’utente ha indicato che dovrebbe annullare l’azione di aggiunta. In questo modo, il gestore dell’evento può occuparsi di respingere il controllore della vista di aggiunta e dire alla vista elenco di aggiornare.
Il confine tra la vista e il presentatore è anche un ottimo posto per ReactiveCocoa. In questo esempio, il controller della vista potrebbe anche fornire metodi per restituire segnali che rappresentano azioni dei pulsanti. Questo permetterebbe al Presenter di rispondere facilmente a quei segnali senza rompere la separazione delle responsabilità.
Routing
I percorsi da uno schermo all’altro sono definiti nei wireframe creati da un interaction designer. In VIPER, la responsabilità del Routing è condivisa tra due oggetti: il Presenter e il wireframe. Un oggetto wireframe possiede i UIWindow
, UINavigationController
, UIViewController
, ecc. È responsabile della creazione di una View/ViewController e della sua installazione nella finestra.
Siccome il Presenter contiene la logica per reagire agli input dell’utente, è il Presenter che sa quando passare a un’altra schermata, e a quale schermata passare. Nel frattempo, il wireframe sa come navigare. Quindi, il Presenter userà il wireframe per eseguire la navigazione. Insieme, descrivono un percorso da una schermata alla successiva.
Il wireframe è anche un posto ovvio per gestire le animazioni di transizione della navigazione. Date un’occhiata a questo esempio dal wireframe di 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
L’applicazione sta usando una transizione di view controller personalizzata per presentare il view controller di add. Poiché il wireframe è responsabile dell’esecuzione della transizione, diventa il delegato di transizione per l’add view controller e può restituire le appropriate animazioni di transizione.
Componenti dell’applicazione che si adattano a VIPER
L’architettura di un’applicazione iOS deve tenere conto del fatto che UIKit e Cocoa Touch sono gli strumenti principali su cui sono costruite le applicazioni. L’architettura deve coesistere pacificamente con tutti i componenti dell’applicazione, ma deve anche fornire linee guida su come alcune parti dei framework sono usate e dove vivono.
Il cavallo di battaglia di un’applicazione iOS è UIViewController
. Sarebbe facile supporre che un concorrente per sostituire MVC eviterebbe di fare un uso pesante dei view controller. Ma i view controller sono centrali per la piattaforma: gestiscono i cambiamenti di orientamento, rispondono agli input dell’utente, si integrano bene con i componenti di sistema come i controller di navigazione, e ora con iOS 7, permettono transizioni personalizzabili tra le schermate. Sono estremamente utili.
Con VIPER, un view controller fa esattamente quello per cui è stato pensato: controlla la vista. La nostra applicazione to-do list ha due view controller, uno per la schermata della lista e uno per la schermata di aggiunta. L’implementazione del view controller per l’aggiunta è estremamente semplice perché tutto quello che deve fare è controllare la vista:
@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
Le applicazioni sono di solito molto più convincenti quando sono connesse alla rete. Ma dove dovrebbe avvenire questo collegamento in rete e cosa dovrebbe essere responsabile di iniziarlo? Di solito spetta all’Interactor avviare un’operazione di rete, ma non gestirà direttamente il codice di rete. Chiederà ad una dipendenza, come un gestore di rete o un client API. L’Interactor potrebbe dover aggregare dati da più fonti per fornire le informazioni necessarie a soddisfare un caso d’uso. Poi sta al Presentatore prendere i dati restituiti dall’Interactor e formattarli per la presentazione.
Un negozio di dati è responsabile di fornire entità a un Interactor. Quando un Interactor applica la sua logica di business, avrà bisogno di recuperare le entità dal data store, manipolare le entità, e poi rimettere le entità aggiornate nel data store. Il negozio di dati gestisce la persistenza delle entità. Le entità non conoscono il data store, quindi le entità non sanno come persistere da sole.
Anche l’Interactor non dovrebbe sapere come persistere le entità. A volte l’Interactor potrebbe voler usare un tipo di oggetto chiamato data manager per facilitare la sua interazione con il data store. Il gestore di dati gestisce più tipi di operazioni specifiche del negozio, come la creazione di richieste di fetch, la costruzione di query, ecc. Questo permette all’Interactor di concentrarsi maggiormente sulla logica dell’applicazione e di non dover sapere nulla su come le entità sono raccolte o persistite. Un esempio di quando ha senso usare un gestore di dati è quando si usa Core Data, che è descritto di seguito.
Ecco l’interfaccia per il data manager dell’applicazione di esempio:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
Quando si usa TDD per sviluppare un Interactor, è possibile sostituire il data store di produzione con un test double/mock. Non parlare con un server remoto (per un servizio web) o toccare il disco (per un database) permette ai vostri test di essere più veloci e ripetibili.
Una ragione per mantenere il data store come un livello distinto con confini chiari è che permette di ritardare la scelta di una specifica tecnologia di persistenza. Se il vostro negozio di dati è una singola classe, potete iniziare la vostra applicazione con una strategia di persistenza di base, e poi passare a SQLite o Core Data più tardi se e quando ha senso farlo, il tutto senza cambiare nient’altro nel codice base della vostra applicazione.
Usare Core Data in un progetto iOS può spesso scatenare più discussioni che l’architettura stessa. Tuttavia, usare Core Data con VIPER può essere la migliore esperienza Core Data che abbiate mai avuto. Core Data è un ottimo strumento per persistere i dati mantenendo un accesso veloce e un basso consumo di memoria. Ma ha l’abitudine di serpeggiare con i suoi NSManagedObjectContext
viticci in tutti i file di implementazione di un’applicazione, in particolare dove non dovrebbero essere. VIPER mantiene il Core Data dove dovrebbe essere: al livello dell’archivio dati.
Nell’esempio dell’elenco delle cose da fare, le uniche due parti dell’applicazione che sanno che i Core Data vengono utilizzati sono il negozio di dati stesso, che imposta lo stack dei Core Data, e il gestore dei dati. Il gestore dei dati esegue una richiesta di fetch, converte i NSManagedObjects
restituiti dal negozio di dati in oggetti standard del modello PONSO, e li passa al livello della logica di business. In questo modo, il nucleo dell’applicazione non dipende mai da Core Data, e come bonus, non ci si deve mai preoccupare di un NSManagedObjects
stantio o poco threaded che rovina i lavori.
Ecco come appare all’interno del gestore dei dati quando viene fatta una richiesta per accedere al negozio 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
Sono controversi quasi quanto Core Data gli Storyboard UI. Gli storyboard hanno molte caratteristiche utili e ignorarli completamente sarebbe un errore. Tuttavia, è difficile realizzare tutti gli obiettivi di VIPER mentre si impiegano tutte le caratteristiche che uno storyboard ha da offrire.
Il compromesso che tendiamo a fare è di scegliere di non usare i segues. Ci possono essere alcuni casi in cui l’uso dei seguiti ha senso, ma il pericolo con i seguiti è che rendono molto difficile mantenere la separazione tra le schermate – così come tra l’UI e la logica dell’applicazione – intatta. Come regola generale, cerchiamo di non usare i segui se l’implementazione del metodo prepareForSegue appare necessaria.
Altrimenti, gli storyboard sono un ottimo modo per implementare il layout della tua interfaccia utente, specialmente quando usi Auto Layout. Abbiamo scelto di implementare entrambe le schermate per l’esempio della lista delle cose da fare usando uno storyboard, e di usare il codice come questo per eseguire la nostra navigazione:
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
Using VIPER to Build Modules
Spesso quando si lavora con VIPER, si trova che una schermata o un insieme di schermate tende a riunirsi come un modulo. Un modulo può essere descritto in diversi modi, ma di solito è meglio pensarlo come una caratteristica. In un’app di podcasting, un modulo potrebbe essere il lettore audio o il browser degli abbonamenti. Nella nostra app lista di cose da fare, la lista e le schermate di aggiunta sono costruite come moduli separati.
Ci sono alcuni vantaggi nel progettare la tua app come un insieme di moduli. Uno è che i moduli possono avere interfacce molto chiare e ben definite, oltre ad essere indipendenti da altri moduli. Questo rende molto più facile aggiungere/rimuovere funzioni, o cambiare il modo in cui l’interfaccia presenta i vari moduli all’utente.
Abbiamo voluto rendere molto chiara la separazione tra i moduli nell’esempio della to-do list, così abbiamo definito due protocolli per il modulo add. Il primo è l’interfaccia del modulo, che definisce ciò che il modulo può fare. Il secondo è il delegato del modulo, che descrive ciò che il modulo ha fatto. Esempio:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Siccome un modulo deve essere presentato per avere molto valore per l’utente, il Presentatore del modulo di solito implementa l’interfaccia del modulo. Quando un altro modulo vuole presentare questo, il suo Presentatore implementerà il protocollo del delegato del modulo, in modo da sapere cosa ha fatto il modulo mentre veniva presentato.
Un modulo potrebbe includere uno strato di logica applicativa comune di entità, interagenti e gestori che può essere usato per più schermate. Questo, naturalmente, dipende dall’interazione tra queste schermate e da quanto sono simili. Un modulo potrebbe facilmente rappresentare solo una singola schermata, come mostrato nell’esempio della lista di cose da fare. In questo caso, il livello di logica dell’applicazione può essere molto specifico per il comportamento del suo particolare modulo.
I moduli sono anche solo un buon modo semplice per organizzare il codice. Tenere tutto il codice di un modulo nascosto nella propria cartella e gruppo in Xcode lo rende facile da trovare quando si ha bisogno di cambiare qualcosa. È una grande sensazione quando si trova una classe esattamente dove ci si aspettava di cercarla.
Un altro vantaggio di costruire moduli con VIPER è che diventano più facili da estendere a più fattori di forma. Avere la logica dell’applicazione per tutti i casi d’uso isolati al livello Interactor permette di concentrarsi sulla costruzione della nuova interfaccia utente per tablet, telefono o Mac, mentre si riutilizza il livello dell’applicazione.
Facendo un ulteriore passo avanti, l’interfaccia utente per le applicazioni iPad può essere in grado di riutilizzare alcune delle viste, dei controller di vista e dei presentatori dell’applicazione iPhone. In questo caso, una schermata dell’iPad sarebbe rappresentata da presentatori e wireframe ‘super’, che comporrebbero lo schermo usando presentatori e wireframe esistenti che sono stati scritti per l’iPhone. Costruire e mantenere un’applicazione su più piattaforme può essere abbastanza impegnativo, ma una buona architettura che promuove il riutilizzo attraverso il modello e il livello dell’applicazione aiuta a rendere questo molto più facile.
Testing con VIPER
Seguire VIPER incoraggia una separazione delle preoccupazioni che rende più facile adottare TDD. L’Interactor contiene una logica pura che è indipendente da qualsiasi UI, il che lo rende facile da guidare con i test. Il Presenter contiene la logica per preparare i dati per la visualizzazione ed è indipendente da qualsiasi widget UIKit. Anche lo sviluppo di questa logica è facile da guidare con i test.
Il nostro metodo preferito è iniziare con l’Interactor. Tutto nell’UI è lì per servire i bisogni del caso d’uso. Usando TDD per testare l’API per l’Interactor, si avrà una migliore comprensione della relazione tra l’UI e il caso d’uso.
Come esempio, guarderemo l’Interactor responsabile della lista dei prossimi impegni. La politica per trovare gli elementi imminenti è di trovare tutti gli elementi da fare entro la fine della prossima settimana e classificare ogni elemento da fare come oggi, domani, più tardi questa settimana o la prossima settimana.
Il primo test che scriviamo è per assicurarci che l’Interactor trovi tutti gli impegni in scadenza entro la fine della prossima settimana:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
Una volta che sappiamo che l’Interactor chiede gli impegni appropriati, scriveremo diversi test per confermare che assegna gli impegni al corretto gruppo di date relative (es.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Ora che sappiamo com’è l’API dell’Interactor, possiamo sviluppare il Presenter. Quando il Presenter riceve dall’Interactor le prossime cose da fare, vogliamo testare di formattare correttamente i dati e di visualizzarli nell’interfaccia utente:
- (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 = ; ];}
Vogliamo anche testare che l’applicazione avvii l’azione appropriata quando l’utente vuole aggiungere una nuova cosa da fare:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
Ora possiamo sviluppare la View. Quando non ci sono impegni imminenti, vogliamo mostrare un messaggio speciale:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
Quando ci sono impegni imminenti da visualizzare, vogliamo assicurarci che la tabella venga mostrata:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Costruire prima l’Interactor è un adattamento naturale al TDD. Se si sviluppa prima l’Interactor, seguito dal Presenter, si può costruire una suite di test intorno a questi livelli e gettare le basi per implementare questi casi d’uso. Potete iterare velocemente su queste classi, perché non dovrete interagire con l’UI per testarle. Poi, quando andrete a sviluppare la View, avrete una logica funzionante e testata e un livello di presentazione da collegare ad essa. Quando avrai finito di sviluppare la View, potresti scoprire che la prima volta che eseguirai l’applicazione tutto funzionerà, perché tutti i tuoi test che passano ti dicono che funzionerà.
Conclusione
Speriamo che questa introduzione a VIPER ti sia piaciuta. Molti di voi ora si staranno chiedendo dove andare dopo. Se voleste progettare la vostra prossima applicazione usando VIPER, da dove comincereste?
Questo articolo e il nostro esempio di implementazione di un’applicazione usando VIPER sono il più specifico e ben definito possibile. La nostra app “to-do list” è piuttosto semplice, ma dovrebbe anche spiegare accuratamente come costruire un’app usando VIPER. In un progetto del mondo reale, quanto da vicino seguirete questo esempio dipenderà dal vostro insieme di sfide e vincoli. Nella nostra esperienza, ognuno dei nostri progetti ha variato leggermente l’approccio all’uso di VIPER, ma tutti hanno tratto grande beneficio dall’uso di VIPER per guidare i loro approcci.
Ci possono essere casi in cui si desidera deviare dal percorso tracciato da VIPER per vari motivi. Forse ti sei imbattuto in un labirinto di oggetti ‘bunny’, o la tua applicazione trarrebbe beneficio dall’uso di segues in Storyboard. Questo va bene. In questi casi, considera lo spirito di ciò che VIPER rappresenta quando prendi la tua decisione. Nel suo nucleo, VIPER è un’architettura basata sul principio di responsabilità unica. Se hai problemi, pensa a questo principio quando decidi come andare avanti.
Ti starai anche chiedendo se è possibile usare VIPER nella tua app esistente. In questo scenario, considera di costruire una nuova funzione con VIPER. Molti dei nostri progetti esistenti hanno preso questa strada. Questo ti permette di costruire un modulo usando VIPER, e ti aiuta anche a individuare eventuali problemi esistenti che potrebbero rendere più difficile adottare un’architettura basata sul principio di responsabilità unica.
Una delle cose belle dello sviluppo del software è che ogni app è diversa, e ci sono anche diversi modi di architettare qualsiasi app. Per noi, questo significa che ogni app è una nuova opportunità per imparare e provare nuove cose. Se decidete di provare VIPER, pensiamo che imparerete anche voi alcune cose nuove. Grazie per aver letto.
Addendum su Swift
La scorsa settimana al WWDC Apple ha presentato il linguaggio di programmazione Swift come il futuro dello sviluppo di Cocoa e Cocoa Touch. È troppo presto per formare opinioni complesse sul linguaggio Swift, ma sappiamo che i linguaggi hanno una grande influenza su come progettiamo e costruiamo il software. Abbiamo deciso di riscrivere la nostra app di esempio VIPER TODO usando Swift per aiutarci a imparare cosa significa questo per VIPER. Finora, ci piace quello che vediamo. Qui ci sono alcune caratteristiche di Swift che pensiamo miglioreranno l’esperienza di costruire applicazioni usando VIPER.
Strutture
In VIPER usiamo piccole, leggere, classi modello per passare dati tra i livelli, come dal Presenter alla View. Queste PONSO sono di solito destinate a trasportare semplicemente piccole quantità di dati, e di solito non sono destinate ad essere sottoclassate. Le struct di Swift sono perfette per queste situazioni. Ecco un esempio di una struct usata nell’esempio Swift di VIPER. Notate che questa struct ha bisogno di essere equiparabile, e quindi abbiamo sovraccaricato l’operatore == per confrontare due istanze del suo tipo:
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
Forse la più grande differenza tra Objective-C e Swift è come i due trattano i tipi. Objective-C è tipizzato dinamicamente e Swift è molto intenzionalmente rigoroso su come implementa il controllo dei tipi in fase di compilazione. Per un’architettura come VIPER, dove un’applicazione è composta da più livelli distinti, la sicurezza dei tipi può essere una grande vittoria per l’efficienza del programmatore e per la struttura architettonica. Il compilatore vi aiuta ad assicurarvi che i contenitori e gli oggetti siano del tipo corretto quando vengono passati tra i confini dei livelli. Questo è un ottimo posto per usare le struct come mostrato sopra. Se una struct è destinata a vivere al confine tra due livelli, allora potete garantire che non sarà mai in grado di fuggire da questi livelli grazie alla sicurezza del tipo.
Altre letture
- VIPER TODO, articolo esempio app
- VIPER SWIFT, articolo esempio app costruito usando Swift
- Counter, un’altra app di esempio
- Mutual Mobile Introduzione a VIPER
- Architettura pulita
- Controllori di vista più leggeri
- Test dei controllori di vista
- Bunnies