Arquitectura: MV, MVVM y modularización
Cómo organizar una app que crece sin que se vuelva un caos. El patrón MV nativo de SwiftUI, cuándo MVVM aún ayuda, y dividir en módulos con Swift Packages.
Una app pequeña cabe en un archivo; una app real necesita estructura. Pero cuidado: importar patrones de otros ecosistemas sin pensar puede añadir complejidad que SwiftUI ya resuelve. Veamos cómo organizar bien, al estilo Swift.
- El patrón MV (Model-View) nativo de SwiftUI.
- Cuándo un ViewModel (MVVM) sigue ayudando.
- Separar la lógica de la vista.
- Modularizar con Swift Packages.
MV: el patrón que SwiftUI hace natural
Con @Observable, tus vistas pueden observar modelos directamente. No siempre necesitas una capa intermedia. El patrón “MV” (Model-View) es simplemente: modelos que contienen datos y lógica, y vistas que los observan y muestran.
@Observable
class BibliotecaModel {
var libros: [Libro] = []
func añadir(_ libro: Libro) { libros.append(libro) }
var pendientes: [Libro] { libros.filter { !$0.leido } }
}
struct BibliotecaView: View {
@State private var modelo = BibliotecaModel()
var body: some View {
List(modelo.pendientes) { Text($0.titulo) }
}
}
Mucha gente llega a SwiftUI desde otros frameworks y crea un “ViewModel” por cada vista por costumbre, aunque solo reenvíe datos. Apple diseñó SwiftUI para que la vista sea ya una especie de view model reactiva: observa el modelo y se redibuja sola. En muchos casos, un ViewModel intermedio solo añade una capa que traduce lo que el modelo ya expone. La pregunta correcta no es “¿dónde pongo el ViewModel?” sino “¿este ViewModel gana algo, o solo repite?”. Empieza simple (MV) y añade capas cuando el dolor lo justifique, no antes.
Cuándo MVVM sí ayuda
Un ViewModel (una clase @Observable dedicada a una vista) tiene sentido cuando esa pantalla tiene lógica de presentación compleja: transformar datos para mostrarlos, coordinar varias fuentes, manejar estados de formulario elaborados. Ahí, sacar esa lógica de la vista a un ViewModel la hace testeable y la vista queda limpia:
@Observable
class RegistroViewModel {
var email = ""
var password = ""
var emailValido: Bool { email.contains("@") }
var puedeEnviar: Bool { emailValido && password.count >= 8 }
func enviar() async { /* lógica de registro */ }
}
La clave: usa MVVM donde aporta (lógica de presentación real), no como regla universal.
Separar la lógica de la vista
Independientemente del patrón, una regla de oro: la lógica de negocio (reglas, cálculos, red, persistencia) no debe vivir dentro del body de una vista. Sácala a modelos, servicios o funciones puras. La vista describe la interfaz; los datos y las reglas viven aparte. Así puedes probar la lógica sin renderizar nada (Nivel 6.3).
Modularizar con Swift Packages
Cuando la app crece, divídela en Swift Packages locales: un módulo para el diseño (componentes UI), otro para el dominio (modelos y lógica), otro por cada feature grande:
Compilación más rápida
Cambiar un módulo recompila solo ese módulo, no toda la app.
Límites claros
Cada módulo expone solo lo público. Las dependencias se ven y se controlan.
Testeable en aislamiento
Pruebas el módulo de dominio sin arrancar la app entera.
Reutilizable
Un módulo de diseño puede servir a la app de iPhone, la de watch y los widgets.
No sobre-arquitectures una app de tres pantallas: un par de archivos bien organizados bastan. Reserva la modularización y los patrones formales para cuando el equipo o la app crecen y el desorden empieza a doler. La mejor arquitectura es la más simple que resuelve tu problema actual — y que puedes hacer crecer cuando el problema crezca.
- Refactoriza una vista sacando su lógica a un modelo
@Observable(MV). - Identifica una pantalla con lógica de presentación compleja y dale un ViewModel.
- Mueve una función de negocio fuera del
bodya una función pura. - Crea un Swift Package local para tus componentes de diseño e impórtalo.