Inyectando DatabaseServiceProtocol en ViewModels

Resumen

Refactorizar tus ViewModels para que dejen de depender de datos mockeados y empiecen a usar un protocolo de base de datos es el paso que separa una app de práctica de una app lista para producción. Aquí aprenderás a aplicar inyección de dependencias con DatabaseServiceProtocol en SwiftUI para que tu código funcione igual con datos mock, SwiftData o Realm.

Por qué reemplazar el mock helper por un DatabaseServiceProtocol

Cuando empiezas un proyecto, es común llamar directamente a un helper como MockRecordsHelper desde el HomeViewModel. Funciona, pero te amarra a una sola fuente de datos. Y aquí viene lo interesante: si mañana quieres cambiar a SwiftData, tendrías que tocar el ViewModel entero.

La solución es declarar una variable databaseService del tipo del protocolo y recibirla como parámetro en el init. Así el ViewModel no sabe ni le importa quién implementa el protocolo, solo lo usa.

¿Qué es la inyección de dependencias en SwiftUI? Es pasar un objeto que cumple un protocolo como parámetro del init, en lugar de crearlo dentro de la clase. Eso te permite intercambiar implementaciones (mock, real, de prueba) sin modificar el ViewModel [02:10].

Cómo refactorizar el HomeViewModel paso a paso

El HomeViewModel tenía dos todos pendientes: getRecords y getTotals. Ambos usaban data mockeada directamente. La estrategia es la misma para los dos: envolver la llamada al servicio en un Task porque las funciones del database service son asíncronas y necesitas usar await dentro de funciones que no son async [01:30].

Cómo traer los registros con fetchRecords

Dentro del Task, llamas a databaseService.fetchRecords() con await para obtener los records. Después, en el MainActor, cambias loading de true a false para que la vista se recargue con los datos reales que devolvió el servicio.

Cómo calcular totales de ingresos y egresos

La función getTotals sigue la misma lógica: un Task, un await al servicio que devuelve una dupla con totalIncome y totalOutcome, y luego en el MainActor se actualiza loadingTotals a false. Con eso la vista refresca y muestra los totales calculados.

Cómo aplicar el mismo patrón en FormRecordViewModel y RecordDetailViewModel

El FormRecordViewModel tenía un detalle importante que corregir antes de seguir: al actualizar un registro no debes generar un nuevo UUID, sino conservar el ID del registro original [03:45]. Generar un nuevo UUID rompe la trazabilidad y causa problemas al sincronizar con la base de datos.

Después de ese fix, el patrón se repite:

  • Declara databaseService: DatabaseServiceProtocol como propiedad.
  • Recibe el protocolo en el init e inicialízalo.
  • Reemplaza las funciones mockeadas por llamadas reales dentro de un Task.
  • Llama a saveNewRecord o updateRecord según corresponda.
  • Si la operación es exitosa, apaga el loading; si falla, muestra el error.

¿Cómo manejo errores cuando falla saveNewRecord? El instructor lo deja como tarea: en el else del resultado, propaga el error al ViewModel y desde ahí actualiza una propiedad @Published que la vista observe para mostrar una alerta [05:20].

El RecordDetailViewModel repite el patrón con la función deleteRecord. Recibe el DatabaseServiceProtocol por inyección, envuelve la llamada en un Task, y al terminar dispara el completion para que la vista navegue o se actualice.

Cómo propagar el DatabaseService desde la app principal hasta cada vista

Al compilar verás errores en el archivo principal de la app porque el HomeViewModel ya no tiene un inicializador vacío. La solución es crear el servicio una sola vez en el entry point de la aplicación e inyectarlo hacia abajo.

swift let databaseService: DatabaseServiceProtocol = MockDatabaseService() HomeView(viewModel: HomeViewModel(databaseService: databaseService))

Desde el HomeView, cuando navegas a FormRecord o a RecordDetail, pasas selfVM.databaseService para que esos ViewModels hereden la misma instancia. Esa cadena es la clave: el servicio se crea al inicio, se inyecta en el Home y desde ahí fluye a cada vista que lo necesite [08:15].

Qué pasar en los previews de SwiftUI

Los #Preview también fallan al compilar porque exigen el protocolo. Aquí es donde el MockDatabaseService brilla: lo pasas en cada preview del HomeView, FormRecord y RecordDetail para que Xcode pueda renderizar las vistas sin conectarse a una base real.

  • HomeView(viewModel: HomeViewModel(databaseService: MockDatabaseService())).
  • FormRecordView(viewModel: FormRecordViewModel(databaseService: MockDatabaseService())).
  • RecordDetailView(viewModel: RecordDetailViewModel(databaseService: MockDatabaseService())).

Qué validar al correr la app después del refactor

Una vez que Command + B te muestre build success, corre la app en el simulador y revisa el flujo completo: filtros del Home, entrada al detalle, edición de un record y guardado. Si todo responde igual que antes, significa que la arquitectura quedó lista para conectarse a una base de datos real como SwiftData o Realm sin tocar los ViewModels.

¿Qué database service vas a implementar primero en tu proyecto? Cuéntame en los comentarios.