É bem conhecido no campo da arquitectura que nós moldamos os nossos edifícios, e depois os nossos edifícios moldam-nos. Como todos os programadores eventualmente aprendem, isto aplica-se igualmente ao software de construção.
É importante desenhar o nosso código para que cada peça seja facilmente identificável, tenha um propósito específico e óbvio, e encaixe com outras peças de uma forma lógica. Isto é o que nós chamamos de arquitetura de software. Boa arquitetura não é o que torna um produto bem sucedido, mas faz com que um produto possa ser mantido e ajuda a preservar a sanidade das pessoas que o mantêm!
Neste artigo, vamos introduzir uma abordagem à arquitetura de aplicação iOS chamada VIPER. VIPER tem sido usado para construir muitos projetos grandes, mas para os propósitos deste artigo, nós estaremos mostrando VIPER através da construção de um aplicativo de lista de afazeres. Você pode acompanhar o projeto de exemplo aqui no GitHub:
O que é VIPER?
Testing nem sempre foi uma parte importante da construção de aplicações iOS. Quando embarcamos em uma busca para melhorar nossas práticas de testes na Mutual Mobile, descobrimos que escrever testes para aplicativos iOS era difícil. Decidimos que se quiséssemos melhorar a forma como testamos nosso software, primeiro teríamos que encontrar uma forma melhor de arquitetar nossos aplicativos. Chamamos esse método de VIPER.
VIPER é uma aplicação de Arquitetura Limpa para aplicações iOS. A palavra VIPER é um backronym para View, Interactor, Apresentador, Entidade, e Routing. Clean Architecture divide a estrutura lógica de uma aplicação em camadas distintas de responsabilidade. Isso facilita o isolamento de dependências (por exemplo, seu banco de dados) e o teste das interações nos limites entre as camadas:
A maioria das aplicações iOS são arquitetadas usando MVC (model-view-controller). Usando MVC como arquitetura de aplicação pode guiá-lo a pensar que cada classe é ou um modelo, uma view, ou um controlador. Já que grande parte da lógica da aplicação não pertence a um model ou view, ela normalmente acaba no controller. Isto leva a um problema conhecido como Massive View Controller, onde os controladores de view acabam fazendo muito. Emagrecer esses controladores de visualização massiva não é o único desafio enfrentado pelos desenvolvedores iOS que procuram melhorar a qualidade de seu código, mas é um ótimo lugar para começar.
VIPER’s distinct layers help with dealing with this challenge by providing clear locations for application logic and navigation related code. Com o VIPER aplicado, você notará que os controladores de visualização em nossa lista de tarefas exemplo são máquinas de controle de visualização lean, mean, view. Você também verá que o código nos controladores de visualização e em todas as outras classes é fácil de entender, mais fácil de testar e, como resultado, também mais fácil de manter.
Concepção da aplicação baseada em casos de uso
Aplicações são frequentemente implementadas como um conjunto de casos de uso. Os casos de uso também são conhecidos como critérios de aceitação, ou comportamentos, e descrevem o que um aplicativo deve fazer. Talvez uma lista precise ser classificável por data, tipo, ou nome. Esse é um caso de uso. Um caso de uso é a camada de uma aplicação que é responsável pela lógica do negócio. Os casos de uso devem ser independentes da implementação da interface de usuário deles. Eles também devem ser pequenos e bem definidos. Decidir como decompor um aplicativo complexo em casos de uso menores é desafiador e requer prática, mas é uma forma útil de limitar o escopo de cada problema que você está resolvendo e cada classe que você está escrevendo.
Construir um aplicativo com VIPER envolve implementar um conjunto de componentes para cumprir cada caso de uso. A lógica da aplicação é uma parte importante da implementação de um caso de uso, mas não é a única parte. O caso de uso também afeta a interface do usuário. Além disso, é importante considerar como o caso de uso se encaixa com outros componentes centrais de uma aplicação, tais como rede e persistência de dados. Os componentes agem como plugins para os casos de uso, e VIPER é uma forma de descrever qual é o papel de cada um desses componentes e como eles podem interagir entre si.
Um dos casos de uso ou requisitos para a nossa aplicação de lista de afazeres era agrupar os afazeres de diferentes maneiras com base na seleção de um usuário. Separando a lógica que organiza esses dados em um caso de uso, somos capazes de manter o código da interface do usuário limpo e embrulhar facilmente o caso de uso em testes para garantir que ele continue a funcionar da maneira que esperamos que funcione.
Peças principais do VIPER
As partes principais do VIPER são:
- View: mostra o que é dito pelo apresentador e relés do usuário de volta ao apresentador.
- Interator: contém a lógica de negócio como especificado por um caso de uso.
- Apresentador: contém a lógica de visualização para preparar o conteúdo para exibição (como recebido do Interactor) e para reagir às entradas do usuário (solicitando novos dados do Interactor).
- Entidade: contém os objetos modelo básicos usados pelo Interactor.
- Roteamento: contém a lógica de navegação para descrever quais telas são mostradas em qual ordem.
Esta separação também está de acordo com o Princípio de Responsabilidade Única. O Interactor é responsável perante o analista de negócio, o Apresentador representa o designer de interacção, e a Vista é responsável perante o designer visual.
Below é um diagrama dos diferentes componentes e como eles estão ligados:
Embora os componentes do VIPER possam ser implementados em uma aplicação em qualquer ordem, optamos por introduzir os componentes na ordem em que recomendamos implementá-los. Você notará que esta ordem é aproximadamente consistente com o processo de construção de uma aplicação inteira, que começa com a discussão do que o produto precisa fazer, seguido de como um usuário irá interagir com ele.
Interactor
Um Interactor representa um único caso de uso na aplicação. Ele contém a lógica de negócio para manipular objetos modelo (Entidades) para realizar uma tarefa específica. O trabalho feito em um Interactor deve ser independente de qualquer IU. O mesmo Interactor pode ser usado num aplicativo iOS ou num aplicativo OS X.
Porque o Interactor é um PONSO (Plain Old NSObject
) que contém principalmente lógica, é fácil de desenvolver usando TDD.
O primeiro caso de uso para o aplicativo modelo é para mostrar ao usuário qualquer item a ser feito (ou seja, qualquer coisa devida até o final da próxima semana). A lógica de negócio para este caso de uso é encontrar qualquer item a ser feito entre hoje e o final da próxima semana e atribuir uma data de vencimento relativa: hoje, amanhã, mais tarde nesta semana ou na próxima semana.
Below é o método correspondente de VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entidades
Entidades são os objetos modelo manipulados por um Interator. As entidades são manipuladas apenas pelo Interactor. O Interactor nunca passa entidades para a camada de apresentação (i.e. Apresentador).
Entidades também tendem a ser PONSOs. Se você estiver usando Core Data, você vai querer que seus objetos gerenciados permaneçam atrás de sua camada de dados. Interactores não devem trabalhar com NSManagedObjects
.
Aqui está a Entidade para o nosso item de tarefas:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Não se surpreenda se as suas entidades são apenas estruturas de dados. Qualquer lógica dependente de aplicação provavelmente estará em um Interactor.
Presenter
The Presenter é um PONSO que consiste principalmente de lógica para conduzir a IU. Ele sabe quando apresentar a interface do usuário. Ele reúne entradas das interações do usuário para que ele possa atualizar a IU e enviar requisições para um Interactor.
Quando o usuário pressiona o botão + para adicionar um novo item de afazer, addNewEntry
é chamado. Para esta ação, o Apresentador pede ao wireframe para apresentar a IU para adicionar um novo item:
- (void)addNewEntry{ ;}
O Apresentador também recebe os resultados de um Interactor e converte os resultados em um formulário que é eficiente para exibir em uma View.
Below é o método que recebe os próximos itens do Interactor. Ele irá processar os dados e determinar o que mostrar ao usuário:
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Entidades nunca são passadas do Interactor para o Apresentador. Ao invés disso, estruturas de dados simples que não têm comportamento são passadas do Interactor para o Apresentador. Isto impede que qualquer ‘trabalho real’ seja feito no Apresentador. O Apresentador só pode preparar os dados para exibição na Vista.
Visualizar
A Vista é passiva. Ele espera que o Apresentador lhe dê conteúdo para ser exibido; ele nunca pede dados ao Apresentador. Métodos definidos para uma View (por exemplo, LoginView para uma tela de login) devem permitir que um Apresentador se comunique em um nível superior de abstração, expresso em termos de seu conteúdo, e não como esse conteúdo deve ser exibido. O Apresentador não sabe sobre a existência de UILabel
, UIButton
, etc. O Apresentador só sabe sobre o conteúdo que mantém e quando deve ser exibido. Cabe ao View determinar como o conteúdo é exibido.
The View é uma interface abstrata, definida no Objective-C com um protocolo. Um UIViewController
ou uma de suas subclasses irá implementar o protocolo View. Por exemplo, a tela ‘add’ do nosso exemplo tem a seguinte interface:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
Views e view controllers também tratam da interação e entrada do usuário. É fácil entender porque os controladores de view geralmente se tornam tão grandes, uma vez que eles são o lugar mais fácil de lidar com essa entrada para executar alguma ação. Para manter nossos controladores de visão enxutos, precisamos dar a eles uma maneira de informar as partes interessadas quando um usuário realiza determinadas ações. O view controller não deve estar tomando decisões baseadas nestas ações, mas deve passar estes eventos para algo que possa.
No nosso exemplo, Add View Controller tem uma propriedade que manipula o evento que está de acordo com a seguinte interface:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
Quando o usuário pressiona o botão cancelar, o view controller diz a este manipulador de eventos que o usuário indicou que ele deve cancelar a ação de adição. Dessa forma, o manipulador de eventos pode tomar conta de descartar o controlador add view e dizer à view list para atualizar.
A fronteira entre a View e o Apresentador também é um ótimo lugar para a ReactiveCocoa. Neste exemplo, o controlador de visualização também pode fornecer métodos para retornar sinais que representam ações do botão. Isto permitiria ao Apresentador responder facilmente a esses sinais sem quebrar a separação de responsabilidades.
Roteamento
Roteamento de uma tela para outra são definidos nos wireframes criados por um designer de interação. Em VIPER, a responsabilidade pelo Roteamento é compartilhada entre dois objetos: o Apresentador, e a wireframe. Um objeto wireframe é dono de UIWindow
, UINavigationController
, UIViewController
, etc. Ele é responsável por criar um View/ViewController e instalá-lo na janela.
Desde que o Apresentador contém a lógica para reagir às entradas do usuário, é o Apresentador que sabe quando navegar para outra tela, e para qual tela navegar. Enquanto isso, o wireframe sabe como navegar. Então, o Apresentador usará a wireframe para realizar a navegação. Juntos, eles descrevem uma rota de um ecrã para o outro.
A wireframe é também um lugar óbvio para lidar com as animações de transição de navegação. Dê uma olhada neste exemplo do 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
O aplicativo está usando uma transição personalizada do controlador de visualização para apresentar o controlador de visualização add. Como o wireframe é responsável por executar a transição, ele se torna o delegado de transição para a controladora de add view e pode retornar as animações de transição apropriadas.
Configuração dos componentes da aplicação com VIPER
Uma arquitetura de aplicação iOS precisa ser considerada pelo fato de que UIKit e Cocoa Touch são as principais ferramentas que as aplicações são construídas em cima. A arquitetura precisa coexistir pacificamente com todos os componentes da aplicação, mas também precisa fornecer diretrizes de como algumas partes dos frameworks são utilizadas e onde eles vivem.
O cavalo de trabalho de uma aplicação iOS é UIViewController
. Seria fácil assumir que um concorrente para substituir MVC se esquivaria de fazer uso pesado de controladores de visualização. Mas os controladores de visualização são centrais para a plataforma: eles lidam com mudanças de orientação, respondem à entrada do usuário, integram-se bem com componentes do sistema como controladores de navegação, e agora com o iOS 7, permitem transições personalizáveis entre telas. Eles são extremamente úteis.
Com VIPER, um controlador de visualização faz exatamente o que era para ser feito: ele controla a visualização. Nosso aplicativo de lista de tarefas tem dois controladores de view, um para a tela de lista, e outro para a tela de adição. A implementação do controller add view é extremamente básica porque tudo que ele tem que fazer é controlar a view:
@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 são normalmente muito mais convincentes quando eles estão conectados à rede. Mas onde essa rede deve ocorrer e o que deve ser responsável por iniciá-la? Normalmente cabe ao Interactor iniciar uma operação de rede, mas ele não vai lidar diretamente com o código de rede. Ele irá perguntar a uma dependência, como um gerente de rede ou cliente API. O Interactor pode ter que agregar dados de várias fontes para fornecer as informações necessárias para atender a um caso de uso. Então cabe ao Apresentador pegar os dados retornados pelo Interactor e formatá-los para apresentação.
Um data store é responsável por fornecer entidades a um Interactor. Como um Interactor aplica sua lógica de negócio, ele precisará recuperar entidades do data store, manipular as entidades e então colocar as entidades atualizadas de volta no data store. O armazenamento de dados gerencia a persistência das entidades. As entidades não sabem sobre o armazenamento de dados, portanto as entidades não sabem como persistir.
O Interator também não deve saber como persistir as entidades. Às vezes o Interactor pode querer usar um tipo de objecto chamado gestor de dados para facilitar a sua interacção com o armazenamento de dados. O gerenciador de dados lida com mais tipos de operações específicas do armazenamento, como a criação de solicitações de fetch, consultas de construção, etc. Isso permite que o Interactor se concentre mais na lógica da aplicação e não tenha que saber nada sobre como as entidades são reunidas ou persistem. Um exemplo de quando faz sentido usar um gerenciador de dados é quando você está usando o Core Data, que é descrito abaixo.
Aqui está a interface do exemplo do gerenciador de dados da aplicação:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
Ao usar TDD para desenvolver um Interactor, é possível mudar o armazenamento de dados de produção com um teste duplo/mock. Não falar com um servidor remoto (para um serviço web) ou tocar no disco (para um banco de dados) permite que seus testes sejam mais rápidos e mais repetíveis.
Um motivo para manter o armazenamento de dados como uma camada distinta com limites claros é que ele permite que você atrase a escolha de uma tecnologia de persistência específica. Se seu armazenamento de dados é uma única classe, você pode iniciar sua aplicação com uma estratégia básica de persistência, e então atualizar para SQLite ou Core Data mais tarde se e quando fizer sentido fazê-lo, tudo sem alterar mais nada na base de código da sua aplicação.
Usar Core Data em um projeto iOS pode muitas vezes desencadear mais debate do que a própria arquitetura. No entanto, usar Core Data com VIPER pode ser a melhor experiência com Core Data que você já teve. Core Data é uma ótima ferramenta para dados persistentes enquanto mantém um acesso rápido e uma pegada de memória baixa. Mas ele tem o hábito de serpentear seus NSManagedObjectContext
tendrils ao longo dos arquivos de implementação de um aplicativo, particularmente onde eles não deveriam estar. VIPER mantém os dados principais onde eles devem estar: na camada de armazenamento de dados.
No exemplo da lista de afazeres, as únicas duas partes do aplicativo que sabem que os Core Data estão sendo usados são o próprio armazenamento de dados, que configura a pilha de Core Data, e o gerenciador de dados. O gerenciador de dados executa uma solicitação de busca, converte o NSManagedObjects
retornado pelo data store em objetos de modelo PONSO padrão, e os passa de volta para a camada de lógica de negócios. Dessa forma, o núcleo da aplicação nunca depende do Core Data, e como um bônus, você nunca precisa se preocupar com dados obsoletos ou mal informados NSManagedObjects
atirando para cima dos trabalhos.
Aqui está o que parece dentro do gerenciador de dados quando uma requisição é feita para acessar o 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
Almost as controversas como Core Data são UI Storyboards. Storyboards têm muitos recursos úteis, e ignorá-los completamente seria um erro. No entanto, é difícil atingir todos os objetivos do VIPER enquanto emprega todas as funcionalidades que um storyboard tem a oferecer.
O compromisso que tendemos a fazer é escolher não usar seguimentos. Pode haver alguns casos em que o uso de segues faz sentido, mas o perigo com segues é que eles tornam muito difícil manter a separação entre telas – assim como entre IU e lógica de aplicação – intacta. Como regra geral, nós tentamos não usar segues se a implementação do método prepareForSegue parecer necessária.
Outra, storyboards são uma ótima maneira de implementar o layout para sua interface de usuário, especialmente enquanto estiver usando o Auto Layout. Nós escolhemos implementar ambas as telas para o exemplo da lista de afazeres usando um storyboard, e usar código como este para realizar nossa própria navegação:
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
Usando VIPER para construir módulos
Muitas vezes quando se trabalha com VIPER, você verá que uma tela ou conjunto de telas tende a se unir como um módulo. Um módulo pode ser descrito de algumas maneiras, mas geralmente é melhor pensado como uma característica. Em um aplicativo podcasting, um módulo pode ser o reprodutor de áudio ou o navegador de assinatura. No nosso aplicativo de lista de afazeres, a lista e as telas de adição são construídas como módulos separados.
Existem alguns benefícios em projetar seu aplicativo como um conjunto de módulos. Um deles é que os módulos podem ter interfaces muito claras e bem definidas, além de serem independentes de outros módulos. Isto torna muito mais fácil adicionar/remover recursos, ou alterar a forma como sua interface apresenta vários módulos para o usuário.
Quisemos tornar a separação entre módulos muito clara no exemplo da lista de afazeres, por isso definimos dois protocolos para o módulo de adição. O primeiro é a interface do módulo, que define o que o módulo pode fazer. O segundo é o delegado do módulo, que descreve o que o módulo fez. Exemplo:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Desde que um módulo tem de ser apresentado para ser de grande valor para o utilizador, o apresentador do módulo normalmente implementa a interface do módulo. Quando outro módulo quer apresentar este, seu Apresentador irá implementar o protocolo de delegação do módulo, para que ele saiba o que o módulo fez enquanto foi apresentado.
Um módulo pode incluir uma camada lógica de aplicação comum de entidades, interautores e gerenciadores que podem ser usados para múltiplas telas. Isto, é claro, depende da interação entre estas telas e de quão semelhantes elas são. Um módulo poderia facilmente representar apenas uma única tela, como é mostrado no exemplo da lista de afazeres. Neste caso, a camada lógica da aplicação pode ser muito específica para o comportamento de seu módulo em particular.
Modules também são apenas uma boa maneira simples de organizar o código. Manter todo o código de um módulo escondido em sua própria pasta e grupo no Xcode torna fácil de encontrar quando você precisa mudar algo. É uma ótima sensação quando você encontra uma classe exatamente onde você esperava procurá-la.
Um outro benefício para construir módulos com VIPER é que eles se tornam mais fáceis de se estender a múltiplos fatores de forma. Ter a lógica do aplicativo para todos os seus casos de uso isolados na camada Interactor permite que você se concentre na construção da nova interface de usuário para tablet, telefone ou Mac, enquanto reutiliza a camada do aplicativo.
Ao levar isso um passo adiante, a interface de usuário para aplicativos iPad pode ser capaz de reutilizar algumas das visualizações, controladores de visualização e apresentadores do aplicativo iPhone. Neste caso, uma tela para iPad seria representada por ‘super’ apresentadores e wireframes, que comporiam a tela usando apresentadores e wireframes existentes que foram escritos para o iPhone. Construir e manter um aplicativo em múltiplas plataformas pode ser bastante desafiador, mas uma boa arquitetura que promova a reutilização através do modelo e da camada de aplicativos ajuda a tornar isso muito mais fácil.
Teste com VIPER
Seguir VIPER encoraja uma separação de preocupações que facilita a adoção de TDD. O Interactor contém lógica pura que é independente de qualquer IU, o que torna mais fácil de conduzir com testes. O Apresentador contém lógica para preparar dados para exibição e é independente de qualquer widget UIKit. Desenvolver esta lógica também é fácil de conduzir com testes.
O nosso método preferido é começar com o Interactor. Tudo na UI está lá para servir as necessidades do caso de uso. Ao usar TDD para testar a API do Interactor, você terá um melhor entendimento da relação entre a IU e o caso de uso.
Como exemplo, veremos o Interactor responsável pela lista de próximos itens a serem feitos. A política para encontrar os próximos itens é encontrar todos os itens a serem feitos até o final da próxima semana e classificar cada item a ser feito como devendo ser feito hoje, amanhã, no final desta semana, ou na próxima semana.
O primeiro teste que escrevemos é para garantir que o Interactor encontre todos os itens a fazer até ao final da próxima semana:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
Após sabermos que o Interactor pede os itens a fazer apropriados, iremos escrever vários testes para confirmar que ele aloca os itens a fazer para o grupo de datas relativas correcto (e.g. hoje, amanhã, etc.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Agora sabemos como é a API para o Interactor, podemos desenvolver o Apresentador. Quando o Apresentador receber os próximos itens do Interactor, vamos querer testar se formatamos os dados corretamente e os exibimos na 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 = ; ];}
Também queremos testar se o aplicativo iniciará a ação apropriada quando o usuário quiser adicionar um novo item para fazer:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
Agora podemos desenvolver a View. Quando não houver itens para fazer, queremos mostrar uma mensagem especial:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
Quando houver itens para fazer, queremos ter certeza que a tabela está mostrando:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Construir o Interactor primeiro é um ajuste natural com TDD. Se você desenvolver o Interactor primeiro, seguido pelo Apresentador, você começa a construir um conjunto de testes em torno dessas camadas primeiro e lançar as bases para a implementação desses casos de uso. Você pode iterar rapidamente nessas classes, porque você não terá que interagir com a IU para testá-las. Então, quando você for desenvolver a View, você terá uma lógica e uma camada de apresentação funcional e testada para se conectar a ela. Quando terminar de desenvolver o View, você poderá descobrir que na primeira vez que executar o aplicativo tudo simplesmente funciona, porque todos os seus testes de aprovação dizem que funcionará.
Conclusion
Esperamos que você tenha gostado desta introdução ao VIPER. Muitos de vocês podem agora estar se perguntando aonde ir a seguir. Se você quisesse arquitetar sua próxima aplicação usando VIPER, por onde você começaria?
Este artigo e nossa implementação de exemplo de uma aplicação usando VIPER são tão específicos e bem definidos quanto nós poderíamos fazê-los. Nosso aplicativo de lista de tarefas é bastante simples, mas também deve explicar com precisão como construir um aplicativo usando o VIPER. Em um projeto do mundo real, o quanto você seguir este exemplo dependerá do seu próprio conjunto de desafios e restrições. Em nossa experiência, cada um de nossos projetos tem variado um pouco a abordagem de utilização do VIPER, mas todos eles têm se beneficiado muito ao utilizá-lo para guiar suas abordagens.
Pode haver casos em que você queira se desviar do caminho traçado pelo VIPER por várias razões. Talvez você tenha encontrado um warren de objetos ‘coelhinhos’, ou sua aplicação se beneficiaria do uso de seguimentos em Storyboards. Tudo bem. Nesses casos, considere o espírito do que o VIPER representa ao tomar a sua decisão. Na sua essência, VIPER é uma arquitetura baseada no Princípio da Responsabilidade Única. Se você está tendo problemas, pense neste princípio ao decidir como avançar.
Você também pode estar se perguntando se é possível usar VIPER no seu aplicativo existente. Neste cenário, considere a construção de uma nova funcionalidade com VIPER. Muitos dos nossos projectos existentes tomaram esta via. Isso permite que você construa um módulo usando VIPER, e também ajuda a identificar quaisquer problemas existentes que possam dificultar a adoção de uma arquitetura baseada no Princípio de Responsabilidade Única.
Uma das grandes coisas do desenvolvimento de software é que cada aplicativo é diferente, e também há diferentes maneiras de arquitetar qualquer aplicativo. Para nós, isso significa que cada aplicativo é uma nova oportunidade de aprender e experimentar coisas novas. Se você decidir experimentar VIPER, pensamos que você também vai aprender algumas coisas novas. Obrigado por ler.
Swift Addendum
Na semana passada na WWDC a Apple introduziu a linguagem de programação Swift como o futuro do desenvolvimento Cocoa e Cocoa Touch. É muito cedo para ter formado opiniões complexas sobre a linguagem Swift, mas sabemos que as linguagens têm uma grande influência na forma como projetamos e construímos software. Decidimos reescrever nosso aplicativo de exemplo VIPER TODO usando o Swift para nos ajudar a aprender o que isso significa para o VIPER. Até agora, nós gostamos do que vemos. Aqui estão algumas características do Swift que sentimos que irão melhorar a experiência de construir aplicativos usando VIPER.
Structs
No VIPER usamos classes de modelos pequenas e leves para passar dados entre camadas, como do Apresentador para o Visualizador. Estas PONSOs são normalmente destinadas a simplesmente transportar pequenas quantidades de dados, e normalmente não se destinam a ser subclassificadas. Estruturas rápidas são um ajuste perfeito para estas situações. Aqui está um exemplo de uma estrutura usada no exemplo VIPER Swift. Observe que essa estrutura precisa ser igualável, e por isso sobrecarregamos o operador == para comparar duas instâncias do seu 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
Talvez a maior diferença entre Objective-C e Swift seja como os dois lidam com os tipos. Objective-C é tipado dinamicamente e Swift é muito intencionalmente rigoroso com a forma como ele implementa a verificação de tipos em tempo de compilação. Para uma arquitetura como VIPER, onde um aplicativo é composto de várias camadas distintas, a segurança do tipo pode ser uma grande vitória para a eficiência do programador e para a estrutura arquitetônica. O compilador está ajudando você a garantir que os recipientes e objetos sejam do tipo correto quando eles estão sendo passados entre os limites das camadas. Este é um ótimo lugar para usar estruturas, como mostrado acima. Se uma estrutura é para viver na fronteira entre duas camadas, então você pode garantir que ela nunca será capaz de escapar entre essas camadas, graças à segurança do tipo.
Outra Leitura
- VIPER TODO, artigo exemplo app
- VIPER SWIFT, artigo exemplo app construído usando Swift
- Counter, outro exemplo de aplicação
- A Introdução ao VIPER
- Arquitectura limpa
- Controladores de visão mais leve
- Controladores de visão de teste
- Bunnies
>