No post anterior vimos um pouco da teoria sobre a arquitetura VIPER, hoje vamos mostrar uma aplicação bem simples, que apresenta uma lista de jogos de tabuleiro, utilizando essa arquitetura. O projeto completo pode ser baixado aqui.

Vamos começar com a nossa hierarquia no projeto Xcode.

Como podemos ver temos uma camada a mais, não falada no post anterior, a camada Repository, como vamos mostrar uma possível maneira de receber dados externos (mockado) e salvar no dispositivo, teremos dois gerenciadores de dados (remoto e local) na camada DataManager e essa nova camada vem para gerenciar as duas anteriores, sendo assim garantimos que o Interactor fique responsável somente pelas regras de negócio e conecte-se apenas à um repositório de dados.

Já na camada View, temos uma pasta chamada Cell, onde está a célula utilizada para mostrar as informações de cada item recebido.

Temos também nossa Model, que contém as informações que vão trafegar entre as camadas e algumas extensions que vão nos auxiliar na criação do módulo VIPER

Vamos ver o que temos em cada camada do VIPER, uma a uma, observando todo o caminho de obtenção de dados, assim como todo o caminho “de volta” até a apresentação dos mesmos na tela do dispositivo.

Pra começar temos que instanciar o nosso módulo VIPER. Todo módulo deve ser instanciado através de seu router, no caso de nossa estrutura utilizamos o método estático “assembleModele()”

static func assembleModule() -> UIViewController {
        let view: GameListViewController = storyboard.instantiateViewController()
        let presenter = GameListPresenter()
        let router = GameListRouter()
        let localDataManager = GameListLocalDataManager()
        let remoteDataManager = GameListRemoteDataManager()
        let repository = GameListRepository(localDataManager: localDataManager, remoteDataManager: remoteDataManager)
        let interactor = GameListInteractor(repository: repository)
        
        view.presenter = presenter
        
        router.viewController = view
        
        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router
        
        interactor.output = presenter
        
        repository.output = interactor
        
        localDataManager.output = repository
        remoteDataManager.output = repository
        
        return view
    }

Podemos notar nesse método que antes dele retornar a controller instanciada, ele configura todo fluxo de entrada e saída de dados.

No caso da primeira controller a ser iniciada, precisamos mudar a entrada do aplicativo, no AppDelegate.Swift para versões de iOS < 13.0 e SceneDelegate.Swift para versões de iOS >= 13.0 , como podemos ver respectivamente nos códigos abaixo.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        window = UIWindow(frame: UIScreen.main.bounds)
        let initialViewController = GameListRouter.assembleModule()
        window?.rootViewController = initialViewController
        window?.makeKeyAndVisible()
        
        return true
    }
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else {
            return
        }
        window = UIWindow(windowScene: windowScene)
        let initialViewController = GameListRouter.assembleModule()
        window?.rootViewController = initialViewController
        window?.makeKeyAndVisible()
        
    }

Assim que a controller é instanciada e a view é carregada, ela “avisa” a presenter que já esta pronta usando a chamada “presenter.viewDidLoad()”

override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        
        presenter.viewDidiLoad()
    }

A presenter pede as informações necessárias para esse módulo à interector com a chamada “interactor.getGames()”

func viewDidiLoad() {
        interactor.getGameList()
    }

Nesse momento a interactor não precisa aplicar nenhuma regra, então simplesmente repassa a chamada para o repository.

func getGameList() {
        repository.getGameList()
    }

O repository por sua vez, no nosso exemplo, tem que verificar se já existe uma lista em cache, então utilizando o método “localDataManager.getGameList()”, caso não retorne uma lista, o repository  faz a chamada para o remoteDataManager para buscar a lista usando “remoteDataManager.getGames()”

func getGameList() {
        if let gameList = localDataManager.getGameList() {
            output.setGameList(gameList)
        } else {
            remoteDataManager.getGameList()
        }
    }

O remoteDataManager é responsável por ir buscar as informações em um serviço externo, no nosso exemplo estamos gerando essas informações mockadas. Após obter as informações começa a “volta” dos dados obtidos chamando o “output.setRemoteGameList(gameList)”. Esse output está implementado no repository conforme o protocolo GameListRemoteDataManagerOutput

weak var output: GameListRemoteDataManagerOutput!
    
    func getGameList() {
        // MOCK
        let gameList = [Game(imageName: "catan", title: "Catan", released: "1995"),
                        Game(imageName: "trajan", title: "Trajan", released: "2011"),
                        Game(imageName: "lisboa", title: "Lisboa", released: "2017"),
                        Game(imageName: "pandemic", title: "Pandemic", released: "2008"),
                        Game(imageName: "ticket_to_ride", title: "Ticket To Ride", released: "2004"),
                        Game(imageName: "azul", title: "Azul", released: "2017"),
                        Game(imageName: "cthulhu", title: "Cthulhu - Death May Die", released: "2019"),
                        Game(imageName: "carcassonne", title: "Carcassonne", released: "2000"),
                        Game(imageName: "pilares_da_terra", title: "Os Pilares da Terra", released: "2006"),
                        Game(imageName: "luna", title: "Luna", released: "2010"),
                        Game(imageName: "dixit", title: "Dixit", released: "2006"),
                        Game(imageName: "dobble", title: "Dobble", released: "2009")]
        
        output.setRemoteGameList(gameList)
    }

Seguindo com a “volta” dos dados, no repository existem duas possibilidades, uma com dados vindo do remoteDataManager, que salva as informações utilizando o localDataManger e em seguida envia os dados recebidos e salvos para a interactor.

func setRemoteGameList(_ gameList: [Game]) {
        saveGameList(gameList: gameList)
        output.setGameList(gameList)
    }

A outra possibilidade é com os dados vindos do localDataManger, dados salvos anteriormente, e nesse caso, o repositor simplesmente repassa os dados para o interactor.

func getGameList() {
        if let gameList = localDataManager.getGameList() {
            output.setGameList(gameList)
        } else {
            remoteDataManager.getGameList()
        }
    }

Em ambas possibilidades, o output chamado esta implementado no interactor e conforme o protocolo GetListRepositoryOutput

output.setGameList(gameList)

Com os dados já na interactor, no nosso exemplo, aplicamos uma regra simples de ordenação e em seguida enviamos os dados já ordenados para presenter utilizando o output nela implementado conforme o GameListInteractorOutput

func setGameList(_ gameList: [Game]) {
        self.gameList = gameList
        sort(gameList: &self.gameList)
        output.setGameList(self.gameList)
    }
private func sort(gameList: inout [Game]) {
        gameList.sort(by: {
            return $0.title < $1.title
        })
    }

A presenter por sua vez, simplesmente envia os dados recebidos (dados já ordenados) e envia para a view, chamando o método “updateTableView(gameList: gameList)”

func setGameList(_ gameList: [Game]) {
        view.updateTableView(gameList: gameList)
    }

A view, recebendo as informações atualiza a tableView mostrando os dados na tela

func updateTableView(gameList: [Game]) {
        self.gameList.append(contentsOf: gameList)
        tableView.reloadData()
    }

Com isso temos um fluxo, simples, mas passando por todas as camadas do VIPER.

Em um próximo post, vamos falar sobre a programação reativa, o que muda um pouco a estrutura dos arquivos de cada camada, facilitando a escrita e o entendimento do fluxo.

Até breve.