¿Cómo se ve MVVM en una app real de SwiftUI?
MVVM en SwiftUI no es una ceremonia de interfaces — es un ViewModel delgado y testeable que expone estado con @Published, una View que solo lo renderiza, y un Model que no sabe nada de ninguno de los dos.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo se ve MVVM en una app real de SwiftUI?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.SwiftUI, se podría decir, empuja a un código base hacia MVVM casi de regalo, así que lo que un entrevistador realmente está preguntando es si podés describir los límites con precisión, y no solo nombrar las tres letras. La View es un struct de SwiftUI: declarativa, descartable, se recrea todo el tiempo, y no carga ninguna lógica de negocio propia. El ViewModel es un ObservableObject (o una clase @Observable en código más nuevo) que guarda el estado de presentación y orquesta los casos de uso. La capa de Model son tipos de datos simples más servicios — red, persistencia — sin ninguna noción de que exista siquiera una interfaz.
Los límites importan más que los nombres, y cada capa tiene algo específico que no puede hacer. Una View nunca debería llamar directamente a una API de red o de persistencia, ni ser dueña de una regla de negocio — "¿es válido este email?" pertenece al ViewModel, no repartido entre un puñado de handlers .onChange en la vista. Un Model tiene que seguir siendo un tipo valor simple y fácil de testear, sin importar SwiftUI ni las partes de Combine orientadas a la interfaz. Y un ViewModel no debería importar para nada el vocabulario de renderizado de SwiftUI — nada de Color, nada de Font, nada de Image. Si un ViewModel necesita expresar "este campo debería verse como un error", expone un valor semántico (un caso de enum, un Bool) y deja que la View decida a qué Color se traduce eso, que es exactamente lo que mantiene al ViewModel testeable sin levantar un simulador.
En concreto, el trabajo de un ViewModel es exponer estado @Published al que la View se bindea, exponer intenciones como funciones simples que la View llama desde acciones de botón (func submit(), func retry(), nunca un cuerpo de lógica metido adentro de un closure de acción de SwiftUI), ser dueño de las dependencias que necesita — un repositorio o servicio, inyectado por su inicializador en vez de buscado como algo global — y traducir errores crudos del modelo o de la red a un estado presentable, típicamente un enum como .idle / .loading / .loaded([Item]) / .failed(String), en vez de dejar que un Error crudo se filtre hasta la View.
Ese último punto sobre la inyección de dependencias es también toda la recompensa de hacer esto bien: como el ViewModel no importa los tipos de renderizado de SwiftUI, es un objeto Swift común que podés testear inyectando un servicio falso y verificando las transiciones de estado @Published, sin simulador ni app anfitriona. Ese suele ser el motivo concreto por el que un entrevistador quiere escuchar MVVM mencionado — no "es lo que hacen los tutoriales", sino "la lógica de negocio está separada del código de render y es demostrablemente testeable".
enum LoginState: Equatable {case idlecase loadingcase successcase failed(String)}protocol AuthService {func login(email: String, password: String) async throws -> Bool}@MainActorfinal class LoginViewModel: ObservableObject {@Published var email: String = ""@Published var password: String = ""@Published private(set) var state: LoginState = .idleprivate let authService: AuthServiceinit(authService: AuthService) {self.authService = authService}var canSubmit: Bool {email.contains("@") && password.count >= 8}func submit() {guard canSubmit else {state = .failed("Ingresá un email válido y una contraseña de 8 o más caracteres.")return}state = .loadingTask {do {let ok = try await authService.login(email: email, password: password)state = ok ? .success : .failed("Credenciales inválidas.")} catch {state = .failed(error.localizedDescription)}}}}struct LoginView: View {@StateObject private var viewModel: LoginViewModelinit(authService: AuthService) {_viewModel = StateObject(wrappedValue: LoginViewModel(authService: authService))}var body: some View {VStack(spacing: 16) {TextField("Email", text: $viewModel.email).textInputAutocapitalization(.never)SecureField("Contraseña", text: $viewModel.password)switch viewModel.state {case .loading:ProgressView()case .failed(let message):Text(message).foregroundStyle(.red)default:EmptyView()}Button("Iniciar sesión", action: viewModel.submit).disabled(!viewModel.canSubmit)}.padding()}}
Vale la pena nombrar dos desviaciones sin que te las pidan, porque señalarlas es lo que separa "seguí un tutorial" de "mantuve esto en un código base real". La primera es el antipatrón del "ViewModel gigante": MVVM no previene el desborde automáticamente, y un ViewModel que absorbe cada responsabilidad de una pantalla — red, formateo, navegación, validación — solo trasladó el problema del Massive View Controller una capa más allá, con otro nombre. El arreglo es la misma disciplina que arreglaba el problema original: extraer colaboradores (un formatter, un validador, un objeto de caso de uso) en vez de dejar que un solo objeto crezca sin límite. La segunda es saber cuándo no recurrir a un ViewModel: una fila sin estado que solo renderiza datos ya calculados más arriba no necesita uno, y aplicar MVVM a cada subvista chiquita agrega ceremonia sin ninguna ganancia de testeabilidad a cambio. Algunos equipos hoy usan deliberadamente MV (una View que observa directamente un Model @Observable, sin ViewModel formal) para pantallas simples, y reservan los ViewModels explícitos para pantallas con orquestación real — vale la pena mencionarlo como el debate vivo que es hoy en la comunidad de SwiftUI, no como una doctrina ya cerrada.
🧠 Check your understanding
0/3 · 0/3 answered1. En una separación estricta de MVVM, ¿qué capa tiene permitido importar tipos de SwiftUI como Color o Image?
2. ¿Por qué es más fácil testear un ViewModel que nunca importa SwiftUI?
3. ¿Qué es el antipatrón del 'ViewModel gigante'?