Na parte 1 do artigo (Qual a melhor arquitetura para aplicações iOS? MVC?) foram traçadas três características que determinam uma boa arquitetura:
- Fácil de usar.
- Responsabilidades distribuídas.
- Pode ser testada.
Vimos que o MVC de acordo com sua origem funcionou e ainda funciona para outros contextos e compreendemos a diferença do MVC da Apple e suas problemáticas.
Nesse artigo, iremos analisar a arquitetura Model-View-ViewModel ou mais conhecida como MVVM.
Antes de falarmos sobre o MVVM para aplicações iOS, vale um entendimento de seu surgimento e quais foram as necessidades.
Em 2005 Jogn Gossman publicou em seu blog o artigo: Introduction to Model/View/ViewModel pattern for building WPF apps. O motivo maior de Jogn é remover completamente a parte lógica da UI, quando analisada a arquitetura em MVC e permitindo que a camada fique totalmente independente da camada view. Baseado em linguagem de marcação (XAML), torna-se muito fácil alterar os elementos de interface quando não possuem regras e aumentando a cobertura de testes.
Tanto o MVVM que Jogn da Microsoft apresentou quanto o Presentation Model em que Martin Fowler publicou em seu artigo, em que cada camada view possui uma camada abstração com algumas regras, os dois tem como objetivo simplificar e realizar a distribuição de responsabilidades. Tornando sua aplicação mais testável e fácil de usar.
Importante:
While several views can utilize the same Presentation Model, each view should require only one Presentation Model. Fonte: Martin Fowler
As views podem utilizar o mesmo Presentation Model, mas cada view só pode ter um único Presentation Model.
MVVM
Vamos analisar o MVVM apresentado por Jogn Gossman:![]() |
Fonte: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel |
Model
Da mesma forma que o MVC, a camada model no MVVM refere-se a camada de dados, representa o conteúdo e pode possuir regras de negócio.View
Assim como o MVC (em teoria quando visto o MVC da Apple) é a camada que possui toda estrutura de UI e a mais próxima do usuário.ViewModel
É a camada intermediária entre a view e model , sendo responsável pela parte lógica da camada view e que possui acesso as informações na camada model.MVVM em Aplicações iOS
Ao trabalharmos com MVVM no desenvolvimento de aplicações para iOS seguimos as mesmas responsabilidades das camadas apresentadas anteriormente: model, view e viewmodel, mas no caso da Apple como visto no parte 1 desse artigo, o controller e a view na prática terminam sendo uma só. Por esse motivo temos a seguinte estrutura da arquitetura MVVM:![]() |
Estrutura MVVM |
O exemplo de aplicação do MVVM utiliza o serviço https://httpbin.org/headers do site httpbin.org. O código faz uma requisição e um parse das informações para apresentar na tela através do componente UITableView do framework UIKit.
Aplicando MVVM
Modelo
Como comentando anteriormente no artigo, a camada modelo refere-se aos dados e pode possuir algumas lógicas. Tomemos como exemplo um modelo que possui duas informações do tipo string.No exemplo criado, criamos a seguinte estrutura:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public class Info { | |
let title: String | |
let detail: String | |
public init(info: String, detail: String) { | |
self.title = info | |
self.detail = detail | |
} | |
public func description() -> String { | |
return "\(title):\(self.detail)" | |
} | |
} |
Nesse artigo não irei apresentar as vantagens de utilizar struct ou class para representar os modelos. Por esse motivo, foi utilizado objeto.
Observe que a classe Info possui as propriedades title e detail que devem ser passadas no construtor. Ela também possui um método chamado description que tem como saída uma string informando o título e detalhes. Nesse caso não foi feito nenhum tratamento, mas poderia ser feito algo como substituir string vazia por alguma outra informação.
ViewModel
Na viewmodel é comum adotar a nomenclatura de VM ou ViewModel no sufixo do nome da classe, para facilitar o desenvolvimento. No nosso exemplo, criamos uma classe chamada MyViewModel que é usada em MyViewController. Seu papel é fazer o transporte das informações entre as camadas View e Model, assim como encontraremos a parte lógica da camada View.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public typealias UpdatedClosure = () -> () | |
public class MyViewModel { | |
private let service = Service() | |
private var data:[CellViewModel] = [] { | |
didSet { | |
DispatchQueue.main.async { | |
self.updatedList?() | |
} | |
} | |
} | |
public var updatedList:UpdatedClosure? | |
public init() {} | |
private func tryFetchData() { | |
self.service.fecthInfo { | |
self.data = $0.map { CellViewModel($0) } | |
} | |
} | |
public func numberOfRows() -> Int { | |
let rows = self.data.count | |
if rows == 0 { | |
self.tryFetchData() | |
} | |
return data.count | |
} | |
public func cellVM(forIndex index: Int) -> CellViewModel { | |
if index < self.data.count { | |
return self.data[index] | |
} | |
return CellViewModel(Info(info:"", detail:"")) | |
} | |
} |
Observe que como pattern, a ViewModel respeita seu conceito e realiza toda parte lógica para a camada View e acesso ao Model. Existem várias formas de fazermos a parte de notificação para que a view possa atualizar o que apresenta em sua interface ao usuário. O mais conhecido é usarmos o KVO.
Key-Value Observing ou KVO da Apple é utilizado para identificarmos as alterações em propriedades específicas de um outro objeto. No caso do exemplo, foi preferível simplificar para focarmos na parte conceitual do pattern. Criamos uma propriedade que é um closure e será executada toda vez que ocorrer uma alteração na lista.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private var data:[CellViewModel] = [] { | |
didSet { | |
DispatchQueue.main.async { | |
self.updatedList?() | |
} | |
} | |
} |
Observação
A camada ViewModel no exemplo do artigo possui chamadas para a camada de serviço, pois em seu conceito do MVVM ele é tratado para resolver o problema em relação a aplicação possuir views com regras/lógicas de negócio e remover a ligação com a camada modelo. O que não impede de ser determinado que a VM (ViewModel) tenha acesso aos serviços.View
Como dito anteriormente sobre a camada view da Apple em relação a construção de telas, pensamos imediatamente na classe UIViewController, do framework UIKit e sabemos que a UIViewController possui em sua dependência uma view. Isso foi tratado na primeira parte do artigo sobre MVC e abaixo uma representação de sua estrutura:![]() |
ViewController da Apple |
No caso do MVVM, quando analisado na construção de tela, tratamos as camadas View e Controller como uma só. Ela irá possuir uma dependência da ViewModel, pois se trata de uma camada que deve fazer somente o papel de apresentação. Veja o código abaixo para entender melhor:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class MyViewController : UIViewController, UITableViewDataSource { | |
private let cellIdentifier = "infoCell" | |
private let tableView = UITableView() | |
private let myViewModel = MyViewModel() | |
private func setupTableView() { | |
self.tableView.register(Cell.self, forCellReuseIdentifier: self.cellIdentifier) | |
self.tableView.rowHeight = 50 | |
self.view.addSubview(self.tableView) | |
self.tableView.translatesAutoresizingMaskIntoConstraints = false | |
self.tableView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true | |
self.tableView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true | |
self.tableView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true | |
self.tableView.heightAnchor.constraint(equalTo: self.view.heightAnchor).isActive = true | |
self.tableView.dataSource = self | |
} | |
override func loadView() { | |
let view = UIView() | |
view.backgroundColor = .white | |
self.view = view | |
self.myViewModel.updatedList = { | |
DispatchQueue.main.async { | |
self.tableView.reloadData() | |
} | |
} | |
self.setupTableView() | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
} | |
//MARK: - UITableViewDataSource | |
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return self.myViewModel.numberOfRows() | |
} | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier, for: indexPath) as! Cell | |
let cellVM = self.myViewModel.cellVM(forIndex: indexPath.row) | |
cell.setup(viewModel: cellVM) | |
return cell | |
} | |
} |
Observe que a classe MyViewController implementa os protocolos UItableViewDataSource, mas não possui nenhuma lógica. Essa parte a ViewModel ficou responsável. Seguindo o objetivo do MVVM, em que cada View possui um ViewModel, as células são representadas pela classe Cell:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Cell: UITableViewCell { | |
func setup(viewModel: CellViewModel) { | |
self.textLabel?.text = viewModel.labelValue() | |
} | |
} |
Como toda View, a classe Cell recebe um ViewModel com a seguinte estrutura:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CellViewModel { | |
private let info: Info | |
init(_ info: Info) { | |
self.info = info | |
} | |
public func labelValue() -> String { | |
return self.info.description() | |
} | |
} |
Essa classe tem acesso ao modelo e possui a lógica para apresentar as informações na camada View através do método labelValue.
Fazendo uma análise baseado nos critérios determinados anteriormente a respeito do que uma boa arquitetura deve ter como característica, temos os seguintes resultados:
- Fácil de usar. ✓
- Responsabilidades distribuídas. ✓
- Pode ser testada. ✓
Conclusão
MVVM foi criado basicamente para diminuir a responsabilidade da camada View e tornar seu código mais testável. Tendo como objetivo, inserir um novo elemento chamado de ViewModel entre as camadas View e Model, possuindo as lógicas que antes ficavam na UI.Referências
- Wikipedia Model-view-viewmodel
- How to implement MVVM (Model-View-ViewModel) in TDD (Test Driven Development)
- Patterns - WPF Apps With The Model-View-ViewModel Design Pattern
- Introduction to Model/View/ViewModel pattern for building WPF apps
- The MVVM Pattern
- Martin Fowler - Presentation Model
- MVVM is Not Very Good
- Introduction to Key-Value Observing Programming Guide