Começando com VIPER

Entendendo e aplicando sem muito sofrimento

Postado por Vitor Santos Mesquita em 14/03/2017

Antes de começarmos gostaria de dizer que sou um entusiasta sobre VIPER e o post é contando um pouco sobre o que sei e entendo e a minha experiência sobre o assunto.


Intro

VIPER já é uma arquitetura bastante conhecida e utilizada pela comunidade iOS. E para quem está começando a pesquisar alguma arquitetura além do famoso MVC pode se surpreender com ela.

Eu comecei a pesquisar sobre VIPER pois tinha algumas grandes dificuldades sobre a arquitetura MVC como: 

  • Desacoplamento de código 
  • Classes muito extensas 
  • Classes muito complexas 
  • Dificuldade na hora de dar manutenção no código
  • Dificuldade em testar

Se você tem algumas desses dificuldades, seja com seu código ou não, VIPER pode te ajudar a soluciona-las. Mas lembrando que ela não irá operar milagres no seu código, na meu caso me ajudou a pensar mais, arquitetar mais, desacoplar mais e assim aumentar meu nível de código.


VIPER é baseada na arquitetura Clean disseminada pelo famoso “tio” Bob (Uncle Bob), que por sua vez é baseada na arquitetura de cebola (Onion architecture) com junção de mais outras 3, onde separa sua aplicação em camadas distintas que são: 

Camada de apresentação (presentation)

É a camada mais superficial dá sua aplicação. Onde o usuário irá interagir, no caso do iOS seria nossas Views, ViewController, TabBarViewContoller …

É a camada responsável por dar “cara” na nossa aplicação, montar tela, mudar cor, mostrar ou esconder elementos entre outras várias coisas relacionadas.

Camada de domínio (domain)

É a camada do coração dá sua aplicação, pois ela é responsável por conter todas as regras de negócios e casos de uso dá sua aplicação.

Camada de dados (data)

É a camada responsável por conversar com o externo dá sua aplicação. Seja API ou Banco de dados, ela que irá manusear toda essa interação com o externo.


Entendido Clean vamos entender o VIPER.

Cada letra do nome representa um tipo de objeto/classe que iremos ter são:

  • View
  • Interactor
  • Presenter
  • Entity
  • Router

Formando V.I.P.E.R!!

View

Representa tudo onde o usuário irá interagir.
Sua responsabilidade é mostrar o que o Presenter manda ela mostrar, e lidar com interações do usuario com a tela passando-as para o Presenter.

import UIKit

/*Protocolo que define as interações Presenter -> View */
protocol ViewProtocol: class {
    func setVisibilityActivityIndicator(isVisible: Bool)
}

class ViewController: UIViewController {
    
    var presenterProtocol: PresenterProtocol!
    
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var tableView: UITableView!
    
    let dispose = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenterProtocol.viewDidLoad()
        bind()
    }
    
    private func bind() {
        presenterProtocol.repositoriesChanged.bindNext {[weak self] (_) in
            guard let strongSelf = self else {return}
            strongSelf.tableView.reloadData()
        }
        .addDisposableTo(dispose)
    }
}

extension ViewController: ViewProtocol {
    
    func setVisibilityActivityIndicator(isVisible: Bool) {
        activityIndicator.isHidden = !isVisible
        if isVisible {
            activityIndicator.startAnimating()
        } else {
            activityIndicator.stopAnimating()
        }
    }
}

Presenter

Representa toda a lógica dá view.
Ele recebe eventos da View e reage a eles.
Ele recebe dados do Interactor, aplica a lógica da View e manda mostrar o que for preciso.

import UIKit
import RxSwift
import RxCocoa

/* Protocolo que define as interações View -> Presenter */
protocol PresenterProtocol {
    var numberOfRows: Int {get}
    var repositoriesChanged: Observable<Void> {get}
    
    func viewDidLoad()
    func getItem(indexPath: IndexPath) -> Repository?
}

class Presenter {
    
    weak var viewProtocol: ViewProtocol?
    var interactorProtocol: RepositoryInput!
    
    var repositories = Variable<[Repository]>([])
}

extension Presenter: PresenterProtocol {
    
    var numberOfRows: Int {
        return self.repositories.value.count
    }
    
    var repositoriesChanged: Observable<Void> {
        return repositories.asObservable().map({ (_) -> Void in
            
        })
    }
    
    func viewDidLoad() {
		viewProtocol?.setVisibilityActivityIndicator(isVisible: true)
		interactorProtocol.searchRepositories(query: "alguma lib")
    }
    
    func getItem(indexPath: IndexPath) -> Repository? {
        return repositories.value[indexPath.row]
    }
}

/* Protocolo de output do interactor */
extension Presenter: RepositoryOutput {
    
    func foundRepositories(repositories: [Repository]?) {
        guard let repositories = repositories else {return}
        viewProtocol?.setVisibilityActivityIndicator(isVisible: false)
        self.repositories.value = repositories
    }
}

Interactor

Representa um caso de uso.
Ele que é responsável por aplicar a lógica de determinado caso.

import UIKit
import RxSwift

/* Protocolo que define as interações Presenter -> Interactor */
protocol RepositoryInput {
    func searchRepositories(query: String)
}

/* Protocolo que define as interações Interactor -> Presenter */
protocol RepositoryOutput: class {
    func foundRepositories(repositories:[Repository]?)
}

class RepositoryInteractor {

    weak var output: RepositoryOutput?
    let dispose = DisposeBag()
}

extension RepositoryInteractor: RepositoryInput {
    
    func searchRepositories(query: String) {
        APIClient
            .getRepositories(byName: query)
            .bindNext {[weak self] (repositories) in
                guard let strongSelf = self else { return }
                strongSelf.output?.foundRepositories(repositories: repositories)
            }
            .addDisposableTo(dispose)
    }
}

Entity

Representa a entidade da API e/ou do Banco

import ObjectMapper

/* Repositorio pego da API do Github */
class Repository: Mappable {
    
    var id: Int?
    var name: String?
    var htmlURLString: String?
    var description: String?
    var starsCount: Int?
    var language: String?
    var forksCount: Int?
    
    required init?(map: Map) {}
    
    func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
        htmlURLString <- map["html_url"]
        description <- map["description"]
        starsCount <- map["stargazers_count"]
        language <- map["language"]
        forksCount <- map["forks_count"]
    }
}

Router

Representa toda a lógica de navegação da sua aplicação.
Ele que sabe para qual tela tem que ir e para qual tem que voltar.
É um objeto composto por 2 (Presenter e WireFrame).

Wireframe

É encarregado de instanciar todas as dependências necessárias.
E ele que também é encarregado de chamar outro WireFrame para assim instanciar sua tala/feature.

import UIKit

/**/
class WireFrame: NSObject {
    
    let viewController = ViewController(nibName:"ViewController", bundle: nil)
    let presenter = Presenter()
    let interactor = RepositoryInteractor()

    override init() {
        super.init()
        viewController.presenterProtocol = presenter
        presenter.viewProtocol = viewController
        presenter.interactorProtocol = interactor
        interactor.output = presenter
    }
    
    func present(window: UIWindow) {
        window.rootViewController = viewController
    }
	
	func showRepositoryDetails(repository: Repository) {
		let wireFrameDetails = RepoDetailsWireFrame(repository: repository)
		wireFrameDetails.present(withVC: viewController)
	}
}

Presenter

Nesse caso é encarregado de falar para o WireFrame fazer algum tipo de navegação, seja para outra tela, como desistanciar a tela corrente.


Dicas

Quando comecei a implementar essa arquitetura tive alguns problemas que deixaram o desenvolvimento um pouco mais complicado, mas com essas dicas pode ficar bem mais simples desenvolver.

Como sabemos no desenvolvimento iOS sempre temos que cuidar a memoria que nossa aplicação consome (famoso Memory Leak). E com o VIPER por ter muitas dependências e dependências ciclicas, tive muito problema em desalocar meu ViewController e suas dependências.

Com a weak var esse problema pode ser facilmente resolvido.

Colocando ponteiros fracos nas dependências ciclicas como segue o diagrama:

Um simples ponteiro fraco causa desalocamento em cadeia e assim não tendo que se preocupar em ficar desalocando dependencia por dependencia na mão.

Mas para criar ponteiros fracos de protocol é necessário falar para o protocolo que somente classes podem implementa-las.

protocol ViewProtocol: class {
}

Para quem está começando com VIPER pode ser meio confuso abstrair o que cada parte deve fazer e/ou implementar. Para isso comecei a separar as camadas do clean em pacotes, como:

Presentation

Nesse pacote deverá conter as dependências view, presenter e router (wireFrame) separados por features.

Domain

Nesse pacote deverá conter o interactor.

Data

Nesse pacote deverá conter as entities e todos as implementações de APIClient ou DataBase

OBS: projeto exemplo que está no meu github

E agora?

Muitas pessoas falam que é muito overhead fazer um software em VIPER, e de fato digo é mesmo, se sua aplicação é pequena.

Mas antes de julgar ou adotar o VIPER, analise bem seu projeto, verique se ele é um projeto que será escalavél e com varias funcionalidades. Se for o VIPER poderá ser uma arma muito útil que irá facilitar sua vida. Se não, viper poderá atrapalhar sua vida e pode acabar em um spaghetti code o seu projeto.

VIPER exige um empenho muito grande de quem está programando para se entender e se aplicar de forma correta.

Abaixo deixarem uns links para ajudar você que está começando em VIPER a se orientar e praticar.

E sempre que precisar de alguma ajuda sobre o assunto pode me contactar no iOSDevBR @vitormes.

Obrigado e espero que gostem!