En el artículo anterior vimos por qué aparecen los Massive View Controller. Como el controlador de vista actúa como View y Controller, todo el código que no tiene dónde ir acaba allí.
La solución más utilizada es MVVM (Model-View-ViewModel). Sin embargo, al revisar código que supuestamente usa MVVM, es frecuente encontrar esto: una clase que solo se llama ViewModel mientras el controlador de vista sigue extrayendo cada valor y aplicándolo a la pantalla.
Hoy aclararemos cuál es el verdadero papel del ViewModel y por qué un MVVM sin binding está incompleto.
El ViewModel es una fábrica de estado de pantalla
Los tres componentes de MVVM se dividen así.
- Model: datos y lógica de negocio (igual que en MVC)
- View: muestra la pantalla. En iOS, tanto UIView como UIViewController pertenecen aquí
- ViewModel: transforma los datos del Model en una forma apta para mostrarse en pantalla y mantiene el estado de la pantalla
Hay dos puntos clave.
Primero, en MVVM, UIViewController pertenece al lado de View. La posición ambigua del controlador de vista queda definida como View. El controlador solo dibuja la pantalla y delega todas las decisiones en el ViewModel.
Segundo, el ViewModel no debe conocer UIKit. Es decir, import UIKitno debe depender de UIKitDate. Recibir un Date y convertirlo en una cadena como «hace 3 minutos», o guardar en un Bool si está cargando, forma parte del ViewModel. Decidir en qué UILabel colocar esa cadena corresponde a View.
Esta separación permite probar el ViewModel sin pantalla. Puedes verificar con lógica pura, sin UIViewController, que «si no hay publicaciones, aparece un mensaje informativo».
¿Por qué está incompleto sin binding?
Si te detienes aquí, el código queda así.
// «MVVM de nombre» sin binding MVVM"
final class ProfileViewController: UIViewController {
let viewModel = ProfileViewModel()
func refresh() {
viewModel.load()
nameLabel.text = viewModel.displayName // Extraer los valores directamente
statusLabel.text = viewModel.statusText // Aplicarlos uno por uno
emptyView.isHidden = !viewModel.isEmpty
}
}
Cada vez que cambia el estado del ViewModel, el controlador de vista debe recordar llamar a refresh(). Si omite un solo momento de llamada, la pantalla y el estado dejan de coincidir. Solo has movido el estado: la «responsabilidad de sincronizar el estado y la pantalla» sigue en el controlador de vista.
Por eso MVVM se diseñó desde el principio con data binding como premisa cuando nació en WPF de Microsoft. Solo está completo cuando se garantiza automáticamente que «si cambia el ViewModel, la View cambia también».
Binding con Combine en UIKit
UIKit no incluye binding integrado, así que añadir Combine es prácticamente el enfoque estándar.
final class ProfileViewModel {
@Published private(set) var displayName = ""
@Published private(set) var isLoading = false
func load() { ... } // Al completarse, @Published actualizar el 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)
}
}
Una vez configurada la suscripción, la pantalla sigue al ViewModel, cambie cuando cambie y como cambie. Desaparece la duda de «¿cuándo debo llamar a refresh?».
En SwiftUI, este binding está integrado en el lenguaje. Si la View solo lee una propiedad de un objeto marcado con @Observable, esa View se vuelve a dibujar automáticamente cuando cambia el valor. Ni siquiera necesitas código de suscripción.
La trampa del Massive ViewModel
Unos meses después de adoptar MVVM aparece un nuevo problema: el ViewModel se vuelve demasiado grande. Si las peticiones de red, la caché y las reglas de negocio entran todas en el ViewModel, solo has cambiado de lugar al monstruo.
El ViewModel debe encargarse únicamente de la lógica de presentación (transformar el estado de pantalla). La obtención de datos corresponde a un Repository o Service, y las reglas de negocio deben bajar a la capa Model. MVVM separa View del resto; no indica que metas todo lo demás en el ViewModel.
Resumen
- El ViewModel transforma los datos del Model en estado para la pantalla y no debe conocer UIKit. Así puedes probarlo sin pantalla.
- Si solo mueves el estado y no hay binding, la responsabilidad de sincronización sigue en el controlador de vista. MVVM se completa con binding.
- En UIKit, Combine se encarga del binding; en SwiftUI, lo hace @Observable.
- Si colocas toda la lógica en el ViewModel, obtienes un Massive ViewModel. Limítalo a la lógica de presentación.
Sin embargo, en SwiftUI la propia View refleja el estado de forma declarativa, por lo que gana fuerza la idea de que «no hace falta ningún ViewModel». Trataremos este debate directamente en el próximo artículo.
Seguir leyendo
- [Arquitectura iOS #5] Por qué las aplicaciones grandes adoptaron y abandonaron la arquitectura iOS VIPER (incluidos los RIBs)
- [Arquitectura iOS #4] Diferencias entre MVP y MVVM: ¿en qué se diferencian Presenter y ViewModel? (Preparación para entrevistas)
- [Arquitectura iOS #8] Guía para elegir una arquitectura iOS según el tamaño del equipo, la vida útil de la aplicación y la complejidad del estado

![Imagen de portada de [Arquitectura iOS #2] Puntos clave de MVVM y el binding](/assets/images/posts/f493544a-c1fe-4846-88cc-edd9a3879c4c/1.jpg)