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.