Saltar al contenido
K
Arquitectura en iOSPaso a paso38 min de lectura · 6 min de máquina Intermedio

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:

FormaCómo esProblema
Crearlo ella mismalet molino = Molino() dentro de la piezaNo se puede sustituir por un falso; cada pieza crea su copia
Buscarlo en un sitio globalpedirlo a una variable que ve todo el mundoNadie sabe quién lo usa; las pruebas se pisan entre sí
Recibirlo de fuerainit(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:

ejemplo mínimo: crear a escondidas frente a recibir (con comentarios)
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 ninguno
Montando el molino
café con café molido
café con polvo de mentira

La 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:

PiezaLo que recibe por el init
PartidoViewModelun any PartidosRepositorio y un notificador opcional
RegistrarGolun any PartidosRepositorio
RepositorioDeArbitrosun any ServicioDelTorneo y un almacén opcional
PublicarCronicaun repositorio de partidos y uno de crónicas
ArbitrosViewModel y CronicaViewModelun 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.swift crea el repositorio de partidos y el notificador.
  • ListaDePartidosView.swift, dentro de .navigationDestination, crea un ServicioJSONPlaceholder() 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.
  • PartidoContenedor reú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.

ejemplo mínimo: un contenedor que monta las piezas (con comentarios)
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á montado
cafeterí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:

CapaCarpetaQué contieneFicheros de Marcador
InterfazUILo que se ve y lo que decide qué se velas vistas (ArbitrosView, CronicaView…) y los ViewModels
DominioDomainLas reglas del negocio y los errores que entiende la app, sin saber de pantallas ni de cómo se hace la llamada a internetel paquete Torneo, RegistrarGol, PublicarCronica, Arbitro con ArbitrosRepositorio, y ErrorDeRed («sin conexión», «el servidor respondió 404»), este en Core/Domain
DatosDataDe dónde salen y dónde se guardan los datosRepositorioDePartidos, RepositorioDeArbitros, RepositorioDeCronicasEnRed, AlmacenDelTorneo, AlmacenDeArbitros
Lo de toda la appCore/Network, Core/NotificationsNo es una capa: lo que no pertenece a ninguna funcionalidadServicioJSONPlaceholder 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:

AlcanceCuánto viveEjemplos
La appDesde que se abre hasta que se cierrael servicio, los repositorios, los almacenes, el caso de uso, el notificador: los guarda el contenedor
La pantallaLo que la vista esté en la pila de navegacióncada 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 cuyo init imprime «Creando cliente HTTP».
  • Almacen: una clase cuyo init imprime «Abriendo almacén».
  • Repositorio: una clase que recibe un cliente y un almacén por el init y los guarda (se pueden leer desde fuera).
  • PantallaViewModel: una clase que recibe un repositorio por el init y 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ón pantallaViewModel() que devuelve un PantallaViewModel nuevo 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: false
Ver solución
solución: un contenedor por capas
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: false

Lo 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.

MarcadorApp (proyecto)
MarcadorApp
ContenedorDeLaApp.swiftse crea
En el Project navigator: un fichero nuevo.

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, un any ServicioDelTorneo.
  • Datos: partidos (un RepositorioDePartidos), arbitros (un RepositorioDeArbitros) y cronicas (un any CronicasRepositorio).
  • Dominio: publicarCronica, el caso de uso.
  • Sistema: notificador, un NotificadorDeSistema.
  • Un init que recibe servicio, almacenDelTorneo y almacenDeArbitros (los dos almacenes opcionales: las vistas previas pasan nil para no tocar el disco) y monta todo en ese orden, cada pieza con las anteriores que necesita. Sin valores por defecto en el init.
  • Tres funciones fábrica, una por ViewModel: partidoViewModel(id:), arbitrosViewModel() y cronicaViewModel(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
MarcadorApp/ContenedorDeLaApp.swift
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.

MarcadorApp (proyecto)
MarcadorApp
MarcadorApp.swiftse modifica
En el Project navigator: un fichero cambia.

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
MarcadorApp/MarcadorApp.swift
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.

MarcadorApp (proyecto)
MarcadorApp
Partidos
UI
ListaDePartidosView.swiftse modifica
Marcador
UI
PartidoEnDirectoView.swiftse modifica
ContentView.swiftse modifica
En el Project navigator: tres ficheros cambian.

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
MarcadorApp/Partidos/UI/ListaDePartidosView.swift
    // 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
MarcadorApp/Partidos/UI/ListaDePartidosView.swift
                case .arbitros:
                    ArbitrosView(viewModel: contenedor.arbitrosViewModel())
                case let .cronica(id):
                    CronicaView(viewModel: contenedor.cronicaViewModel(id: id))
MarcadorApp/Partidos/UI/ListaDePartidosView.swift
#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
MarcadorApp/Marcador/UI/PartidoEnDirectoView.swift
    // 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)
    }
MarcadorApp/Marcador/UI/PartidoEnDirectoView.swift
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
MarcadorApp/ContentView.swift
#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

  1. 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».
  2. 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.
  3. 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, en ContenedorDeLaApp.swift. Por eso, aunque abras Árbitros diez veces, todas las visitas comparten el mismo repositorio; lo único nuevo en cada visita es el ViewModel.
  4. 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ún ServicioJSONPlaceholder() fuera de MarcadorApp.swift y 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 LIBERADO en 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é ContenedorDeLaApp es el único que las conoce todas.

Comprueba lo que entendiste

6 preguntas · necesitas 5 para completar la lección

  1. 1.¿Qué es la inyección de dependencias?

  2. 2.Antes de esta lección, cada vez que se abría «Árbitros» nacía un repositorio nuevo. ¿Por qué?

  3. 3.¿Qué es la raíz de composición?

  4. 4.En ContenedorDeLaApp, ¿por qué los repositorios son propiedades y los ViewModels son funciones que devuelven uno nuevo?

  5. 5.¿Qué ocurre en un contenedor con lazy var cliente = ClienteHTTP() si nadie lee cliente?

  6. 6.¿En qué capa está PublicarCronica en la arquitectura de Marcador?

Faltan 6 de 6