Desenvolvimento

27 set, 2016

Criando um aplicativo com boas práticas em Swift – Parte 02

Publicidade

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())
                    
                }

***

Artigo publicado originalmente em: http://blog.concretesolutions.com.br/2016/08/aplicativo-em-swift-parte-2/