wandres.dev
DE LA IDEA A LA APP STORE · el panorama completo

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.

⏱ 14 min

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.

🎯 Al terminar esta lección sabrás
  • 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) }
    }
}
No copies MVVM de otros mundos sin pensar

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.

💡
Arquitectura proporcional al tamaño

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.

⚔️ Estructura con criterio
  1. Refactoriza una vista sacando su lógica a un modelo @Observable (MV).
  2. Identifica una pantalla con lógica de presentación compleja y dale un ViewModel.
  3. Mueve una función de negocio fuera del body a una función pura.
  4. Crea un Swift Package local para tus componentes de diseño e impórtalo.