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.
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.
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
- [Arquitetura iOS #5] Por que grandes aplicativos adotaram e abandonaram a arquitetura iOS VIPER (incluindo RIBs)
- [Arquitetura iOS #4] Diferenças entre MVP e MVVM: qual é a diferença entre Presenter e ViewModel? (Preparação para entrevistas)
- [Arquitetura iOS #8] Guia para escolher uma arquitetura iOS considerando o tamanho da equipe, a vida útil do app e a complexidade do estado

![Imagem de capa de [Arquitetura iOS #2] Principais pontos de MVVM e binding](/assets/images/posts/f493544a-c1fe-4846-88cc-edd9a3879c4c/1.jpg)