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/



