Gestão Dev & TIARTIGO

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

Já publiquei a primeira parte desta série. Hoje, vou mostrar uma arquitetura MVP aplicada em Swift.

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/

Desenvolvedor iOS na Concrete Solutions, estudou Análise e Desenvolvimento de Sistemas no Infnet (RJ). Já tentou desenvolver em Java, mas foi pelo iOS que se apaixonou de verdade. Nas horas vagas, não dispensa cozinhar acompanhado de uma boa música brasileira.

Ver perfil