Já publiquei a primeira parte desta série. Hoje, vou mostrar uma arquitetura MVP aplicada em Swift. A utilização do MVP no projeto contribuiu para uma melhor segregação de responsabilidade e melhor arrumação do código e mais testabilidade, mas gostaria de levar a discussão da utilização de uma arquitetura mais à frente, deixando meu entendimento após diversas discussões sobre elas.
Quando falo sobre aplicação de arquiteturas, sempre prefiro ser comedido. O MVP em um pequeno projeto pode ser mais organizado, mas a literatura em geral não favorece quanto à aplicação de animações e à utilização da Presenter para diversas views, o que pode culminar em um Presenter massivo, por exemplo. Nesses casos, um dos maiores conhecimentos que se pode tirar proveito é o SOLID. Esses princípios poderão levá-lo a criar uma arquitetura ideal a sua necessidade.
Neste projeto, o model não possui grandes funções; ele apenas implementa o protocolo Mappable para o parse de JSON para o objeto. Na camada de view, teremos o protocolo ShotsView, e nele serão assinadas as funções que serão implementadas pela ShotsViewController. Esse protocolo também permitirá a criação de um mock que facilitará na validação dos testes da presenter, que veremos à frente.
Protocolo:
protocol ShotsView: AnyObject { func showLoading() func hideLoading() func showError(message: String) func setShots(shots: [Shot]) func showEmptyView() }
Implementação:
extension ShotsViewController: ShotsView { func showLoading() { HUD.show(.Progress) } func hideLoading() { HUD.hide() } func showError(message: String) { HUD.flash(.Label(message), delay: 3.0) { _ in } } func setShots(shots: [Shot]) { collectionView?.hidden = false dataSource = shots } func showEmptyView() { collectionView?.hidden = true } }
Por fim, o Presenter. Sendo um mediador entre o model e a view, ele é responsável pelo controle dos dados e estados da view e atualização do modelo. Com uma propriedade do tipo ShotsView, que é atribuida pela ViewController que implementa o protocolo, o Presenter saberá quando chamar cada estado da view.
class ShotPresenter { weak private var shotsView: ShotsView? func setView(shotsView: ShotsView) { self.shotsView = shotsView } func getShots<Service: Gettable where Service.DataArray == [Shot]>(fromService service: Service) { shotsView?.showLoading() service.get { result in switch result { case .success(let shots): if shots.isEmpty { self.shotsView?.showEmptyView() self.shotsView?.hideLoading() } else { self.shotsView?.setShots(shots) self.shotsView?.hideLoading() } case .failure(let error): self.shotsView?.showError(error.message()) self.shotsView?.showEmptyView() self.shotsView?.hideLoading() } } } }
Esse talvez seja o melhor momento para falar sobre o Gettable. Vindo do conceito de Orientação a Protocolo, ele serve para adicionar comportamento ao serviço. Nesse caso, ele permite ao serviço obter dados e, caso necessário, poderíamos adicionar outros comportamentos possíveis, como o de deletar.
Em Gettable, usamos o associatedtype para manter um espaço reservado para um tipo usado como parte do protocolo que é especificado quando o protocolo é implementado.
protocol Gettable { associatedtype DataArray func get(completion: Result<DataArray, Errors> -> Void) }
Essa abordagem, junto com a injeção de dependência, como no caso do serviço do método getShots, também nos ajudará com os testes a seguir.
Para os testes criaremos nossos Mocks de serviço, o ShotServiceMock, e de view, o ShotViewMock. Ambos se aproveitarão dos recursos que citei acima. O ShotViewMock se aproveita do protocolo criado na arquitetura MVP para implementação da Controller, e com ele validaremos se as chamadas do presenter ocorreram como esperávamos.
class ShotViewMock: ShotsView { var wasLoading = false var dataSource: [Shot]? = nil var messageError: String? = nil var showError = false var hideEmptyView = true func showLoading() { wasLoading = true } func hideLoading() { wasLoading = false } func showError(message: String) { messageError = message } func setShots(shots: [Shot]) { dataSource = shots } func showEmptyView() { hideEmptyView = false } }
O ShotServiceMock se aproveita do conceito de Orientação a Protocolo, permitindo implementar Gettable e responder o que se espera.
class ShotServiceMock: Gettable { var callError = false var callEmpty = false var wasCalled = false func get (completion: Result<[Shot], Errors> -> Void) { wasCalled = true let firstShot = Shot(id: 1, title: "Title 1", description: "Description 1", images: Images(high: NSURL(), normal: NSURL(), teaser: NSURL()), view: 10, like: 100, comment: 1000, user: User(id: 1, name: "User 1", avatar: NSURL(), location: "Location 1")) let secondShot = Shot(id: 2, title: "Title 2", description: "Description 2", images: Images(high: NSURL(), normal: NSURL(), teaser: NSURL()), view: 20, like: 200, comment: 2000, user: User(id: 2, name: "User 2", avatar: NSURL(), location: "Location 2")) if callError { completion(Result.failure(Errors.undefinedError(description: "Testando falha"))) } else { if callEmpty { completion(Result.success(Array<Shot>())) } else { completion(Result.success([firstShot, secondShot])) } } } }
Como a função getShots da presenter espera um serviço que implemente Gettable e que o associatedType seja do tipo [Shot], representado por <Service: Gettable whereService.DataArray == [Shot]>, basta que nosso ShotServiceMock siga essas condições para ser passado como dependência.
let presenter: ShotPresenter = ShotPresenter() let service: ShotServiceMock = ShotServiceMock() var view: ShotViewMock = ShotViewMock() describe("Valid View from Presenter") { beforeEach { view = ShotViewMock() presenter.setView(view) } it("valid show view") { presenter.getShots(fromService: service) expect(view.hideEmptyView).to(equal(true)) expect(view.wasLoading).to(equal(false)) expect(view.dataSource).toNot(beNil()) expect(view.dataSource?.count).to(equal(2)) } }
Assim, garantimos que cada ponto do getShots está sendo testado dependendo das situações que tivermos. Por exemplo, se o array de Shots retornar vazio, o hideEmptyView deverá ser falso.
it("valid empty view") { service.callEmpty = true presenter.getShots(fromService: service) expect(view.hideEmptyView).to(equal(false)) expect(view.wasLoading).to(equal(false)) expect(view.dataSource).to(beNil()) }
E assim finalizo esse compilado de práticas que podem ajudar na organização, testabilidade e arquitetura. Esse código está disponível no git, e lá você poderá verificar como se comporta a aplicação, observar mais testes e outros trechos de códigos não colocados no artigo.
Ficou alguma dúvida ou tem algo a acrescentar? Fique à vontade nos campos abaixo.
Até a próxima!
***
Artigo publicado originalmente em: http://blog.concretesolutions.com.br/2016/08/aplicativo-em-swift-parte-2/