Engenharia iOS

[Arquitetura iOS #2] Principais pontos de MVVM e binding

No artigo anterior, vimos por que surgem os Massive View Controllers. Como o view controller exerce os papéis de View e Controller, todo código sem um lugar definido acaba ali.

4 min de leitura
Imagem de capa de [Arquitetura iOS #2] Principais pontos de MVVM e binding

No artigo anterior, vimos por que surgem os Massive View Controllers. Como o view controller exerce os papéis de View e Controller, todo código sem um lugar definido acaba ali.

A solução mais usada é MVVM (Model-View-ViewModel). Porém, ao abrir um código que adotou MVVM, é comum encontrar isto: uma classe que só tem o nome ViewModel, enquanto o view controller continua extraindo cada valor e aplicando-o na tela.

Hoje vamos esclarecer o papel real do ViewModel e por que um MVVM sem binding fica pela metade.


ViewModel é uma fábrica de estado da tela

Os três componentes do MVVM se dividem assim.

  • Model: dados e lógica de negócio (igual ao MVC)
  • View: exibição da tela. No iOS, tanto UIView quanto UIViewController pertencem a este lado
  • ViewModel: transforma os dados do Model em um formato adequado para exibição na tela e mantém o estado da tela

Há dois pontos principais.

Primeiro, no MVVM, UIViewController fica no lado da View. A posição ambígua do view controller discutida no artigo anterior é definida de vez como View. O view controller apenas desenha a tela e delega todas as decisões ao ViewModel.

Segundo, o ViewModel não deve conhecer UIKit. Isso significa que import UIKitnão pode haver dependência de UIKitDate. Receber um Date e convertê-lo em uma string como “há 3 minutos”, além de armazenar em um Bool se está carregando, faz parte do ViewModel. Escolher em qual UILabel colocar essa string é responsabilidade da View.

Essa separação permite testar o ViewModel sem uma tela. Você pode verificar, usando lógica pura e sem UIViewController, que “quando não há posts, uma mensagem informativa aparece”.


Por que ele fica pela metade sem binding?

Se você parar aqui, o código ficará assim.

// “MVVM só de fachada” sem binding MVVM"
final class ProfileViewController: UIViewController {
    let viewModel = ProfileViewModel()

    func refresh() {
        viewModel.load()
        nameLabel.text = viewModel.displayName   // Extrair os valores diretamente
        statusLabel.text = viewModel.statusText  // Aplicá-los um a um
        emptyView.isHidden = !viewModel.isEmpty
    }
}

Sempre que o estado do ViewModel muda, o view controller precisa se lembrar de chamar refresh(). Se você esquecer uma única chamada, a tela e o estado ficam dessincronizados. Você apenas moveu o estado; a “responsabilidade de sincronizar o estado e a tela” continua no view controller.

É por isso que o MVVM foi projetado desde o início com data binding como premissa quando nasceu no WPF da Microsoft. Ele só fica completo quando “a View muda junto com o ViewModel” é garantido automaticamente.

Configure uma assinatura e a tela acompanhará o estado automaticamente
Configure uma assinatura e a tela acompanhará o estado automaticamente

Binding com Combine no UIKit

O UIKit não oferece binding integrado, então adicionar Combine é praticamente o padrão.

final class ProfileViewModel {
    @Published private(set) var displayName = ""
    @Published private(set) var isLoading = false

    func load() { ... }  // Ao concluir, @Published  atualizar o valor
}

final class ProfileViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.$displayName
            .assign(to: \.text!, on: nameLabel)
            .store(in: &cancellables)
        viewModel.$isLoading
            .sink { [weak self] in self?.spinner.isAnimating = $0 }
            .store(in: &cancellables)
    }
}

Depois de configurar a assinatura, a tela acompanha o ViewModel, independentemente de quando ou como ele mudar. A dúvida “quando devo chamar refresh?” simplesmente desaparece.

No SwiftUI, esse binding faz parte da linguagem. Basta a View ler uma propriedade de um objeto marcado com @Observable para que ela seja redesenhada automaticamente quando o valor mudar. Nem código de assinatura é necessário.


A armadilha do Massive ViewModel

Alguns meses após adotar MVVM, surge um novo problema: o ViewModel fica inchado. Se requisições de rede, cache e regras de negócio forem todos parar no ViewModel, você apenas mudou o monstro de lugar.

O ViewModel deve cuidar apenas da lógica de apresentação (processamento do estado da tela). A busca de dados deve ficar no Repository ou Service, e as regras de negócio devem descer para a camada Model. MVVM separa a View do restante; não é um padrão para colocar todo o restante no ViewModel.

Se você colocar tudo ali, apenas mudará o monstro de lugar
Se você colocar tudo ali, apenas mudará o monstro de lugar

Resumo

  • O ViewModel transforma os dados do Model em estado para a tela e não deve conhecer UIKit. Assim, ele pode ser testado sem uma tela.
  • Se você mover apenas o estado e não houver binding, a responsabilidade pela sincronização continuará no view controller. O MVVM só fica completo com binding.
  • No UIKit, o Combine cuida do binding; no SwiftUI, essa função é do @Observable.
  • Se toda a lógica for colocada no ViewModel, você terá um Massive ViewModel. Limite-o à lógica de apresentação.

No SwiftUI, porém, a própria View reflete o estado de forma declarativa, então ganha força a ideia de que “não é preciso ter um ViewModel”. Vamos discutir esse debate diretamente no próximo artigo.

Continue lendo