Inyección de dependencias en todas las capas
Al terminar: Montar el contenedor de la app por capas y hacerlo llegar a las vistas por @Environment.
Al terminar, Marcador tendrá un contenedor, ContenedorDeLaApp, que es el único sitio donde se crean y se conectan las piezas de la app: el servicio de red, los almacenes del disco, los repositorios, el caso de uso y los ViewModels. Ninguna pantalla volverá a crear nada «a escondidas». La app se verá y se comportará igual que antes, y las mismas pruebas seguirán en verde.
Importa porque una pieza que crea sus propias dependencias no se puede probar con falsos ni se puede cambiar sin tocar su código, y porque cuando la app crece, nadie sabe dónde nace cada cosa. Temas: qué es una dependencia y qué es inyectarla, la raíz de composición, el contenedor por capas, los alcances (app y pantalla), @Environment para repartirlo, y el nombre de la arquitectura que ya tienes: MVVM + repositorio + casos de uso.
Recibir en vez de crear
Una dependencia es cualquier cosa que una pieza necesita para hacer su trabajo. El ArbitrosViewModel necesita un repositorio; el repositorio necesita un servicio y un almacén; el servicio necesita una URLSession. Es una cadena.
Hay tres formas de que una pieza consiga lo que necesita:
| Forma | Cómo es | Problema |
|---|---|---|
| Crearlo ella misma | let molino = Molino() dentro de la pieza | No se puede sustituir por un falso; cada pieza crea su copia |
| Buscarlo en un sitio global | pedirlo a una variable que ve todo el mundo | Nadie sabe quién lo usa; las pruebas se pisan entre sí |
| Recibirlo de fuera | init(moledor: Moledor) | Hay que pasarlo, pero se ve y se puede cambiar |
La tercera se llama inyección de dependencias. El nombre asusta más que la idea: la pieza deja de crear lo que necesita y lo recibe, normalmente por el init.
Un ejemplo con una cafetera, en dos versiones:
final class Molino {
init() { print("Montando el molino") } // para ver CUÁNDO se crea
func moler() -> String { "café molido" }
}
// A) La cafetera crea su molino ella misma. No hay forma de darle otro.
final class CafeteraAEscondidas {
private let molino = Molino()
func preparar() -> String { "café con \(molino.moler())" }
}
// B) La cafetera RECIBE un moledor por el init (inyección). Lo conoce por protocolo.
protocol Moledor { func moler() -> String }
extension Molino: Moledor {} // el molino de verdad cumple el contrato:
// la extensión va vacía porque `Molino` ya tiene `moler()`
struct MoledorDeMentira: Moledor { func moler() -> String { "polvo de mentira" } }
final class Cafetera {
private let moledor: any Moledor
init(moledor: any Moledor) { self.moledor = moledor }
func preparar() -> String { "café con \(moledor.moler())" }
}
print(CafeteraAEscondidas().preparar()) // monta un molino de verdad cada vez
print(Cafetera(moledor: MoledorDeMentira()).preparar()) // aquí no se monta ningunoMontando el molino
café con café molido
café con polvo de mentiraLa versión B se puede probar con un molino de mentira; la A, no. Es lo mismo que haces desde Tests en iOS: falsos y espías con RepositorioFalso.
Tu código ya lo hace
Sin llamarla así, ya has usado la inyección en cada lección de arquitectura:
| Pieza | Lo que recibe por el init |
|---|---|
PartidoViewModel | un any PartidosRepositorio y un notificador opcional |
RegistrarGol | un any PartidosRepositorio |
RepositorioDeArbitros | un any ServicioDelTorneo y un almacén opcional |
PublicarCronica | un repositorio de partidos y uno de crónicas |
ArbitrosViewModel y CronicaViewModel | un repositorio / el caso de uso |
Y en las pruebas, RepositorioFalso, NotificadorEspia, ServicioFalso y ArbitrosRepositorioFalso son las piezas que se pasan en lugar de las de verdad.
El problema que queda: ¿quién las monta?
Alguien tiene que crear todas esas piezas y conectarlas. Hoy lo hacen tres sitios distintos, y en dos de ellos «a escondidas» (la pieza crea lo que necesita dentro de sí misma, sin que quien la use lo sepa):
MarcadorApp.swiftcrea el repositorio de partidos y el notificador.ListaDePartidosView.swift, dentro de.navigationDestination, crea unServicioJSONPlaceholder()y un repositorio de árbitros cada vez que se abre la pantalla, y lo mismo con la crónica. Mira los comentarios «OJO» que dejaste.PartidoContenedorreúne el repositorio y el notificador del entorno para montar el ViewModel del partido.
Tres consecuencias: cada apertura de la pantalla de árbitros crea otro repositorio (y otro servicio); si un día cambias el servidor, hay que buscar todos los ServicioJSONPlaceholder() del proyecto; y el que lea ListaDePartidosView no sabe de qué depende la app.
El contenedor: un único sitio que monta todo
La solución es que un solo sitio cree las piezas, una vez, y las reparta. A ese sitio se le llama raíz de composición (composition root) y, cuando es una clase que guarda las piezas, contenedor. Todo lo demás recibe lo que necesita y no crea nada.
Un contenedor pequeño con la cafetera, para ver su forma. Este bloque usa las clases del ejemplo anterior: pégalo debajo de él en tu Playground (las tres primeras líneas que salgan serán las de aquel ejemplo). Las propiedades son lazy: cada pieza se crea la primera vez que alguien la pide, y una sola vez.
final class Cafeteria {
// `lazy var`: se crea al leerla por primera vez y se guarda; la segunda vez devuelve la misma.
lazy var molino = Molino()
// La cafetera RECIBE el molino que el contenedor ya tiene.
lazy var cafetera = Cafetera(moledor: molino)
}
let cafeteria = Cafeteria()
print("cafetería lista") // todavía no se ha montado nada
print(cafeteria.cafetera.preparar()) // aquí se monta el molino, una vez
print(cafeteria.cafetera.preparar()) // la segunda vez, ya está montadocafetería lista
Montando el molino
café con café molido
café con café molido«Montando el molino» sale una sola vez, aunque preparemos dos cafés.
Las capas de Marcador, con nombre
Con la red, los casos de uso y el contenedor, la app tiene ya una arquitectura completa. Se llama MVVM + repositorio + casos de uso y se reparte en tres capas:
| Capa | Carpeta | Qué contiene | Ficheros de Marcador |
|---|---|---|---|
| Interfaz | UI | Lo que se ve y lo que decide qué se ve | las vistas (ArbitrosView, CronicaView…) y los ViewModels |
| Dominio | Domain | Las reglas del negocio y los errores que entiende la app, sin saber de pantallas ni de cómo se hace la llamada a internet | el paquete Torneo, RegistrarGol, PublicarCronica, Arbitro con ArbitrosRepositorio, y ErrorDeRed («sin conexión», «el servidor respondió 404»), este en Core/Domain |
| Datos | Data | De dónde salen y dónde se guardan los datos | RepositorioDePartidos, RepositorioDeArbitros, RepositorioDeCronicasEnRed, AlmacenDelTorneo, AlmacenDeArbitros |
| Lo de toda la app | Core/Network, Core/Notifications | No es una capa: lo que no pertenece a ninguna funcionalidad | ServicioJSONPlaceholder y los DTO (Core/Network), Notificador (Core/Notifications) |
Las dependencias apuntan hacia abajo: la interfaz usa el dominio (un ViewModel llama al caso de uso). El dominio, en cambio, no conoce a los datos: solo conoce los protocolos de los repositorios (ArbitrosRepositorio, PartidosRepositorio, CronicasRepositorio), y son los datos los que los cumplen. El protocolo va en Domain si está en el fichero del modelo (Arbitro.swift) y en Data si comparte fichero con la clase que lo cumple. Así el caso de uso usa a los repositorios sin saber quiénes son, y en las pruebas se le dan falsos. El contenedor es el único que conoce todas las capas, porque es el que las conecta.
Alcances: cuánto vive cada pieza
No todas las piezas viven lo mismo:
| Alcance | Cuánto vive | Ejemplos |
|---|---|---|
| La app | Desde que se abre hasta que se cierra | el servicio, los repositorios, los almacenes, el caso de uso, el notificador: los guarda el contenedor |
| La pantalla | Lo que la vista esté en la pila de navegación | cada ViewModel: se crea al abrir la pantalla y se libera al salir |
Por eso el contenedor guarda los repositorios en propiedades (let), pero para los ViewModels tiene funciones fábrica (arbitrosViewModel(), cronicaViewModel(id:)) que devuelven uno nuevo cada vez. Si los repositorios fueran por pantalla, cada pantalla tendría el suyo y podrían tener listas distintas: una refrescaría los árbitros y la otra seguiría enseñando los antiguos. Y si los ViewModels fueran de la app, el estado de una visita (un aviso, una ruedecita) se quedaría pegado a la siguiente.
Cómo llega el contenedor a las vistas
Con lo que ya sabes de @Environment: la raíz de la app crea el contenedor y lo pone en el entorno con .environment(contenedor); cualquier vista lo lee con @Environment(ContenedorDeLaApp.self). Para que sea una clase observable que se pueda poner en el entorno, el contenedor lleva @Observable.
Reto · Ejercicio: un contenedor por capas
En tu Playground (reutiliza el mismo y borra lo anterior), monta un contenedor con tres capas. Piezas:
ClienteHTTP: una clase cuyoinitimprime «Creando cliente HTTP».Almacen: una clase cuyoinitimprime «Abriendo almacén».Repositorio: una clase que recibe un cliente y un almacén por elinity los guarda (se pueden leer desde fuera).PantallaViewModel: una clase que recibe un repositorio por elinity lo guarda (se puede leer desde fuera).Contenedor: una clase con el cliente, el almacén y el repositorio, los tres creados la primera vez que se leen (lazy var, como en la cafetería); el repositorio recibe el cliente y el almacén del propio contenedor. Y una funciónpantallaViewModel()que devuelve unPantallaViewModelnuevo en cada llamada, con el repositorio del contenedor.
Crea un contenedor, imprime «contenedor listo», pide dos PantallaViewModel y comprueba con === (la comparación «¿es el mismo objeto en memoria?», no «¿son iguales?»; solo sirve con clases) si comparten cliente y si son la misma pantalla. Lo que debe salir:
contenedor listo
Creando cliente HTTP
Abriendo almacén
mismo cliente: true
misma pantalla: falseVer solución
final class ClienteHTTP {
init() { print("Creando cliente HTTP") }
}
final class Almacen {
init() { print("Abriendo almacén") }
}
final class Repositorio {
let cliente: ClienteHTTP
let almacen: Almacen
init(cliente: ClienteHTTP, almacen: Almacen) {
self.cliente = cliente
self.almacen = almacen
}
}
final class PantallaViewModel {
let repositorio: Repositorio
init(repositorio: Repositorio) { self.repositorio = repositorio }
}
final class Contenedor {
// red
lazy var cliente = ClienteHTTP()
// datos
lazy var almacen = Almacen()
lazy var repositorio = Repositorio(cliente: cliente, almacen: almacen)
// interfaz: una fábrica, un objeto nuevo en cada llamada
func pantallaViewModel() -> PantallaViewModel {
PantallaViewModel(repositorio: repositorio)
}
}
let contenedor = Contenedor()
print("contenedor listo") // todavía no se ha creado nada
let primera = contenedor.pantallaViewModel() // aquí se crean el cliente y el almacén
let segunda = contenedor.pantallaViewModel()
print("mismo cliente: \(primera.repositorio.cliente === segunda.repositorio.cliente)")
print("misma pantalla: \(primera === segunda)")contenedor listo
Creando cliente HTTP
Abriendo almacén
mismo cliente: true
misma pantalla: falseLo de la red y los datos tiene alcance de app: se crean una vez, y las dos pantallas comparten cliente. Lo de la interfaz tiene alcance de pantalla: cada llamada trae un ViewModel nuevo. Es justo el reparto de ContenedorDeLaApp.
Paso 1 · El contenedor: ContenedorDeLaApp.swift
Objetivo: un fichero nuevo con todas las piezas montadas por capas. Todavía no lo usa nadie.
ContenedorDeLaApp une todo y lo conoce todo, así que no es de ninguna funcionalidad: se queda en la raíz, junto a MarcadorApp.swift. Clic derecho sobre la carpeta MarcadorApp (la de dentro, la que contiene MarcadorApp.swift) → New File from Template… → Swift File → nombre ContenedorDeLaApp (solo el target MarcadorApp).
Fíjate en que la app ya está ordenada por funcionalidad: cada una (Marcador, Partidos, Arbitros, Cronica…) tiene sus propias capas, igual que en Android. Va en Core lo que no pertenece a ninguna funcionalidad, aunque lo usen varias; si es de una, se queda en ella aunque otras lo lean. El contenedor es lo único que las ve todas.
Qué debe tener, capa por capa:
- Es una clase final, observable y aislada al hilo principal (
@MainActor, como los ViewModels). - Red:
servicio, unany ServicioDelTorneo. - Datos:
partidos(unRepositorioDePartidos),arbitros(unRepositorioDeArbitros) ycronicas(unany CronicasRepositorio). - Dominio:
publicarCronica, el caso de uso. - Sistema:
notificador, unNotificadorDeSistema. - Un
initque recibeservicio,almacenDelTorneoyalmacenDeArbitros(los dos almacenes opcionales: las vistas previas pasannilpara no tocar el disco) y monta todo en ese orden, cada pieza con las anteriores que necesita. Sin valores por defecto en elinit. - Tres funciones fábrica, una por ViewModel:
partidoViewModel(id:),arbitrosViewModel()ycronicaViewModel(id:).
¿Por qué sin valores por defecto? La razón de fondo es de diseño: quien crea el contenedor decide el servidor y dónde se guarda (la app pasa los de verdad; una vista previa pasa nil), y un valor por defecto escondería esa decisión en el init. Además, si lo pruebas, Xcode avisa: main actor-isolated static property 'predeterminado' can not be referenced from a nonisolated context. Lo que dice el aviso es que predeterminado solo debe usarse desde el hilo principal (el que dibuja la pantalla), y el compilador no da por hecho que un valor por defecto, que se calcula donde se llama al init, esté ahí. No hace falta entenderlo más hoy: sin valores por defecto, el aviso no existe.
Intenta escribirlo tú; Cmd+B debe compilar.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
import Observation
// EL CONTENEDOR: la RAÍZ DE COMPOSICIÓN de la app (el gemelo de los módulos de Hilt de Android: `RedModule`, `DatosModule` y `RepositoriosModule`).
//
// Es el ÚNICO sitio que crea las piezas y las conecta entre sí. Cada pieza RECIBE lo que necesita
// por su `init` (inyección de dependencias) en vez de BUSCARLO o crearlo a escondidas. Así se crea
// cada pieza UNA sola vez, y en las pruebas basta con pasar otras (falsas).
//
// Está ordenado por capas, de abajo arriba:
// red el servicio (con su única `URLSession`)
// datos los repositorios, cada uno con su almacén en el disco
// dominio los casos de uso
// interfaz una fábrica por pantalla: cada pantalla recibe un ViewModel NUEVO
//
// Alcances: el contenedor y lo que guarda viven lo que la app (un `@State` de `MarcadorApp`); cada
// ViewModel vive lo que su pantalla (un `@State` de su vista).
//
// `@Observable`: para poder repartirlo por el entorno con `.environment(contenedor)`.
@MainActor
@Observable
final class ContenedorDeLaApp {
// ---- red ----
let servicio: any ServicioDelTorneo
// ---- datos ----
let partidos: RepositorioDePartidos
let arbitros: RepositorioDeArbitros
let cronicas: any CronicasRepositorio
// ---- dominio ----
let publicarCronica: PublicarCronica
// ---- sistema ----
let notificador: NotificadorDeSistema
// Sin valores por defecto: quien crea el contenedor decide el servidor y dónde se guarda
// (la app pasa los almacenes de `Documents`; las vistas previas, `nil`, para no tocar el disco).
init(servicio: any ServicioDelTorneo, almacenDelTorneo: AlmacenDelTorneo?, almacenDeArbitros: AlmacenDeArbitros?) {
self.servicio = servicio
partidos = RepositorioDePartidos(almacen: almacenDelTorneo)
arbitros = RepositorioDeArbitros(servicio: servicio, almacen: almacenDeArbitros)
cronicas = RepositorioDeCronicasEnRed(servicio: servicio)
publicarCronica = PublicarCronica(partidos: partidos, cronicas: cronicas)
notificador = NotificadorDeSistema()
}
// ---- interfaz: un ViewModel por pantalla ----
func partidoViewModel(id: Int) -> PartidoViewModel {
PartidoViewModel(partidoId: id, repositorio: partidos, notificador: notificador)
}
func arbitrosViewModel() -> ArbitrosViewModel {
ArbitrosViewModel(repositorio: arbitros)
}
func cronicaViewModel(id: Int) -> CronicaViewModel {
CronicaViewModel(partidoId: id, publicarCronica: publicarCronica)
}
}Dos diferencias con el contenedor de la cafetería. Aquí las piezas son let, no lazy var: se crean todas en el init, porque son baratas y así el orden de montaje está a la vista. Y la URLSession no aparece: el servicio usa la compartida del sistema (.shared), que ya es una sola para toda la app.
Paso 2 · La raíz de la app crea el contenedor
Objetivo: que MarcadorApp.swift cree el contenedor una vez, con los almacenes de verdad, y lo reparta por el entorno.
Se sustituyen los dos @State de antes (repositorio y notificador, con sus comentarios) por uno solo, el del contenedor: borra los dos. El -reiniciarDatos YES sigue funcionando, ahora dentro del bloque que crea el contenedor. Y como las demás vistas siguen leyendo el repositorio de partidos y el notificador del entorno, la raíz los saca del contenedor y los pone también: son las mismas piezas, no copias.
Intenta escribirlo tú, en el struct MarcadorApp: un @State privado, contenedor, que se inicializa con un bloque (el que ya tienes con -reiniciarDatos) y devuelve un ContenedorDeLaApp con un ServicioJSONPlaceholder() y los dos almacenes .predeterminado; y en el body, ContentView() con tres .environment(…): el contenedor, contenedor.partidos y contenedor.notificador.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import SwiftUI
// `@main` marca el punto de entrada: es el `Application` de Android (`MarcadorApplication`)
// y la `MainActivity` juntos. Un `App` no dibuja nada por sí mismo: declara ESCENAS.
//
// El asistente de Xcode lo llama `MarcadorAppApp` (nombre del proyecto + «App»).
// Aquí se renombra a `MarcadorApp` porque el módulo ya se llama así.
@main
struct MarcadorApp: App {
// `@Environment(\.scenePhase)` lee en qué fase está la ESCENA: `.active` (delante y
// recibiendo toques), `.inactive` (visible pero sin interacción: llamada, selector de
// apps) y `.background` (fuera de la vista). Es el `onStart/onStop` de Android, pero de
// la escena y no de una pantalla.
@Environment(\.scenePhase) private var fase
// EL CONTENEDOR, creado UNA vez al arrancar: crea el servicio, los repositorios, el caso de uso y
// el notificador, y se reparte a las pantallas por el entorno.
// Con el torneo guardado en `Documents/torneo.json` y los árbitros en `Documents/arbitros.json`
// (`-reiniciarDatos YES` borra los dos al arrancar).
@State private var contenedor: ContenedorDeLaApp = {
if Configuracion.reiniciarDatos {
AlmacenDelTorneo.predeterminado.borrar()
AlmacenDeArbitros.predeterminado.borrar()
UserDefaults.standard.removeObject(forKey: Ajustes.claveDuracion)
}
return ContenedorDeLaApp(
servicio: ServicioJSONPlaceholder(),
almacenDelTorneo: .predeterminado,
almacenDeArbitros: .predeterminado
)
}()
var body: some Scene {
// `WindowGroup` es la escena normal de una app de iOS: una ventana con esta vista raíz.
WindowGroup {
ContentView()
.environment(contenedor)
// Las vistas que ya leían el repositorio de partidos o el notificador los siguen
// encontrando en el entorno: son las mismas piezas, sacadas del contenedor.
.environment(contenedor.partidos)
.environment(contenedor.notificador)
}
// `onChange(of:)` con dos parámetros (antiguo, nuevo) es la forma de iOS 17+.
.onChange(of: fase) { antigua, nueva in
Registro.anotar("scenePhase: \(antigua) -> \(nueva)")
}
}
}Compila con Cmd+B. La app funciona igual: el contenedor aún no lo lee nadie, pero ya manda las mismas piezas por el entorno.
Paso 3 · Las pantallas dejan de montar cosas
Objetivo: que las tres pantallas pidan sus ViewModels al contenedor.
ListaDePartidosView.swift. Gana un @Environment(ContenedorDeLaApp.self) y los destinos .arbitros y .cronica se reducen a una línea cada uno: desaparecen los comentarios «OJO» y los ServicioJSONPlaceholder(). La vista previa, además, construye un contenedor sin almacenes.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// La lista LEE los partidos del repositorio (`@Observable`): al registrar un gol, la fila cambia sola.
@Environment(RepositorioDePartidos.self) private var repositorio
// El contenedor fabrica los ViewModels de las pantallas que se apilan.
@Environment(ContenedorDeLaApp.self) private var contenedor case .arbitros:
ArbitrosView(viewModel: contenedor.arbitrosViewModel())
case let .cronica(id):
CronicaView(viewModel: contenedor.cronicaViewModel(id: id))#Preview {
// Un contenedor sin almacenes: la vista previa no lee ni escribe en el disco.
let contenedor = ContenedorDeLaApp(servicio: ServicioJSONPlaceholder(), almacenDelTorneo: nil, almacenDeArbitros: nil)
ListaDePartidosView()
.environment(contenedor)
.environment(contenedor.partidos)
.environment(contenedor.notificador) // lo lee el partido al navegar
}El resto del fichero no cambia: son tres trozos, en el orden en que aparecen (las propiedades de la vista, el destino de la navegación, y la vista previa del final). Sustituyen a lo que había en cada sitio.
PartidoEnDirectoView.swift. La vista del partido deja de recibir partidoId, repositorio y notificador, y recibe su ViewModel ya montado. Y PartidoContenedor (el padre que se usaba para enseñar el bug de @ObservedObject) se lo pide al contenedor.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// La vista RECIBE su ViewModel ya montado (lo fabrica el contenedor): no sabe con qué repositorio
// ni con qué notificador se creó.
init(viewModel: PartidoViewModel) {
_viewModel = State(wrappedValue: viewModel)
}struct PartidoContenedor: View {
let id: Int
@Environment(ContenedorDeLaApp.self) private var contenedor
@State private var redibujados = 0
var body: some View {
VStack(spacing: 0) {
HStack {
Spacer()
Button("Redibujar el padre (\(redibujados))") { redibujados += 1 }
.buttonStyle(.bordered)
}
.padding([.horizontal, .top], 16)
PartidoEnDirectoView(viewModel: contenedor.partidoViewModel(id: id))
}
.navigationTitle("Partido n.º \(id)")
.navigationBarTitleDisplayMode(.inline)
}
}Dos trozos: el init de PartidoEnDirectoView (sustituye al anterior, que recibía tres argumentos) y el struct PartidoContenedor entero. El resto del fichero no cambia.
¿Y quién más llamaba al init viejo? Nadie: solo PartidoContenedor, y ya lo has cambiado. Puedes comprobarlo con Find → Find in Project… (Cmd+Shift+F), buscando PartidoEnDirectoView(: debe salir una sola línea, la de PartidoContenedor, y ninguna en #Preview. Si sale otra con el init viejo, cámbiala igual.
Una duda razonable: cada vez que el padre se redibuja, contenedor.partidoViewModel(id: id) crea un ViewModel nuevo. Pero State(wrappedValue:) solo se queda con el primero (La pantalla pinta el uiState); los siguientes se descartan. Por eso el botón «Redibujar el padre» sigue sin reiniciar el partido.
ContentView.swift. Solo cambia la vista previa del final, que ahora usa el contenedor.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
#Preview {
// La lista y el partido leen el contenedor, el repositorio y el notificador del entorno: aquí se los
// da la vista previa, con un contenedor sin almacenes (no toca el disco).
let contenedor = ContenedorDeLaApp(servicio: ServicioJSONPlaceholder(), almacenDelTorneo: nil, almacenDeArbitros: nil)
ContentView()
.environment(contenedor)
.environment(contenedor.partidos)
.environment(contenedor.notificador)
}Compila con Cmd+B: Build Succeeded, sin avisos.
Míralo funcionar
- Ejecuta la app (Cmd+R). Todo se ve y se hace como antes: la lista, un partido en directo, «Ver goleadores», «Árbitros» con su lista y su aviso, y «Crónica» con «Publicar».
- Entra en un partido y sal con el botón de atrás. En la consola de Xcode (Cmd+Shift+Y) aparece
PartidoViewModel LIBERADO: el ViewModel de la pantalla se creó con la pantalla y se liberó con ella, aunque lo fabrique el contenedor. Es el alcance de pantalla. - Comprueba que solo se crea un repositorio de árbitros. Busca en el proyecto (Cmd+Shift+F)
RepositorioDeArbitros(: fuera de las pruebas debe salir una sola vez, enContenedorDeLaApp.swift. Por eso, aunque abras Árbitros diez veces, todas las visitas comparten el mismo repositorio; lo único nuevo en cada visita es el ViewModel. - Ejecuta las pruebas con Cmd+U: las mismas de antes (50, o 53 si tienes ejercicios con pruebas propias).
Lo que viene: en Equipos y plantillas en iOS la barra de «Partidos» gana un botón «Equipos», y el repositorio aprende a añadir jugadores a un equipo. El contenedor que acabas de montar no cambia: sus ViewModels siguen naciendo por pantalla.
Punto de control
- Existe
ContenedorDeLaApp.swift, y en el proyecto ya no queda ningúnServicioJSONPlaceholder()fuera deMarcadorApp.swifty de las vistas previas. - Cmd+B termina en
Build Succeeded, sin avisos, y Cmd+U sigue dando 50 pruebas en verde (con tus ejercicios de pruebas, 53; sin ellos, 50): 34 unitarias y 16 de interfaz. Los ejercicios con pruebas propias, como la taquilla, suman tres más: no es un error. - La app se comporta igual que antes y al salir de un partido sale
PartidoViewModel LIBERADOen la consola. - Sabes decir de qué capa es cada fichero de la tabla (interfaz, dominio o datos), cuánto vive el contenedor y cuánto un ViewModel, y por qué
ContenedorDeLaAppes el único que las conoce todas.
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.¿Qué es la inyección de dependencias?
2.Antes de esta lección, cada vez que se abría «Árbitros» nacía un repositorio nuevo. ¿Por qué?
3.¿Qué es la raíz de composición?
4.En
ContenedorDeLaApp, ¿por qué los repositorios son propiedades y los ViewModels son funciones que devuelven uno nuevo?5.¿Qué ocurre en un contenedor con
lazy var cliente = ClienteHTTP()si nadie leecliente?6.¿En qué capa está
PublicarCronicaen la arquitectura de Marcador?