Dos fuentes: la pantalla y las pruebas
Al terminar: Pintar siempre lo guardado, avisar cuando la red falla, probar el repositorio y mirarlo funcionar.
Esta es la segunda mitad de la pareja que empezó Dos fuentes: el almacén y el repositorio. Hoy conectas lo que escribiste: al terminar, la pantalla de árbitros pintará siempre lo guardado, al instante y también sin internet; la red solo refrescará la lista, al abrir la pantalla, con el botón «Actualizar» y al tirar de la lista hacia abajo. Si la red falla, verás un aviso naranja encima de la lista y la lista no se vaciará. Y lo comprobarás con pruebas y mirándolo funcionar.
Importa porque es la parte que el usuario ve: de nada sirve guardar en el disco si la pantalla sigue esperando a internet. Temas: cuándo se refresca, un struct como estado de pantalla (varias cosas a la vez), .refreshable, cambiar un contrato y arreglar a quienes lo usan, y probar un repositorio con un fichero real en una carpeta temporal.
Cuándo se refresca
Tener el disco no dice cuándo ir a la red. Hay cuatro momentos habituales:
| Momento | Cómo se hace en iOS |
|---|---|
| Al abrir la pantalla | .task { … }, que ya conoces de .task frente a .onAppear |
| A mano, con un botón | un botón «Actualizar» que lanza la misma función |
| Tirando de la lista hacia abajo | .refreshable { … } (nuevo hoy) |
| Cada cierto tiempo, con la app cerrada | BGTaskScheduler: solo se nombra, es un tema aparte |
.refreshable recibe un bloque asíncrono. Mientras el bloque se ejecuta, SwiftUI enseña su propia ruedecita arriba de la lista, y la quita cuando el bloque termina. Por eso el bloque tiene que esperar a la red: await viewModel.refrescar().
Un estado con varias cosas a la vez
En Red en Marcador: la lista de árbitros el estado de la pantalla era un enum: o «cargando», o «error», o «éxito». Un enum solo está en un caso cada vez, y eso ya no vale. Con dos fuentes pueden darse varias cosas a la vez:
- hay una lista guardada que se está viendo,
- se está refrescando (ruedecita),
- y el refresco anterior falló (aviso).
Por eso el estado pasa a ser un struct con tres propiedades, arbitros, refrescando y error. (Si vienes de Android: es el mismo paso de sealed interface a data class.)
// ANTES: un solo caso a la vez. «Hay lista Y falló el refresco» es imposible de expresar.
enum EstadoDeMenuAntes {
case cargando
case error(String)
case exito([String])
}
// DESPUÉS: tres datos independientes. Todas las combinaciones caben.
struct EstadoDeMenu: Equatable {
var platos: [String] = [] // valor por defecto: lista vacía
var refrescando = false // valor por defecto: no
var error: String? // nil: no hay error (un texto opcional)
}
// Lista guardada, refrescando, y el refresco anterior falló: las tres a la vez.
let estado = EstadoDeMenu(platos: ["Paella"], refrescando: true, error: "Sin conexión")
print(estado)EstadoDeMenu(platos: ["Paella"], refrescando: true, error: Optional("Sin conexión"))error es un String?, un texto que puede no existir (Opcionales). Por eso print lo enseña como Optional("Sin conexión"): es la forma de escribir «hay un texto, y es este». En la pantalla lo abrirás con if let, y sale solo «Sin conexión».
Paso 1 · El contrato: Arbitro.swift y RepositorioDeArbitros.swift
Objetivo: que el contrato de los árbitros tenga las dos cosas (leer y refrescar) y que el repositorio nuevo lo cumpla; el repositorio viejo, el de solo red, se va.
Primero, Arbitro.swift: el protocolo pasa a pedir una propiedad que se lee y una función que refresca. El modelo y la extensión con init(dto:) no cambian (ya es Codable desde la lección anterior).
Intenta escribirlo tú: en ArbitrosRepositorio (sigue siendo solo para clases: : AnyObject, como en Red en Marcador: la lista de árbitros), sustituye la función arbitros() por una propiedad arbitros que solo se lee ({ get }) y una función asíncrona refrescar que puede lanzar un error y no devuelve nada.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// EL MODELO: lo que la app entiende por «árbitro», con nombres en español y solo lo que usa.
// La pantalla pinta `Arbitro`, nunca el DTO: si mañana el servidor cambia un nombre, solo cambia
// la traducción de abajo.
// `Codable`: además se guarda en el disco (`arbitros.json`) y se vuelve a leer.
struct Arbitro: Identifiable, Codable, Equatable {
let id: Int
let nombre: String
let ciudad: String
}
extension Arbitro {
// De DTO a modelo: el ÚNICO sitio que conoce los dos.
init(dto: ArbitroDTO) {
self.init(id: dto.id, nombre: dto.name, ciudad: dto.address.city)
}
}
// EL REPOSITORIO de los árbitros, con DOS FUENTES: el disco y el servidor.
// - `arbitros`: lo guardado. Se lee al instante, también sin red.
// - `refrescar()`: pide la lista al servidor, la guarda y entonces cambia `arbitros`.
protocol ArbitrosRepositorio: AnyObject {
var arbitros: [Arbitro] { get }
func refrescar() async throws
}{ get } dice que la propiedad se puede leer desde fuera; el protocolo no obliga a que se pueda escribir. Por eso RepositorioDeArbitros, con su private(set) var arbitros, lo cumple.
Ahora el repositorio. En RepositorioDeArbitros.swift cambia solo la línea de la clase para que declare que cumple el contrato; el resto del fichero no cambia:
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
@Observable
final class RepositorioDeArbitros: ArbitrosRepositorio {Y borra el fichero RepositorioDeArbitrosEnRed.swift (clic derecho → Delete → Move to Trash): su única fuente era la red y ya no tiene sitio. En Xcode saldrán errores donde se usaba (ListaDePartidosView, ArbitrosViewModel): son los de los pasos siguientes.
Paso 2 · El ViewModel y la pantalla
Objetivo: que la pantalla pinte el estado nuevo (un struct) y tenga los tres modos de refrescar.
En el ViewModel desaparecen cargar() y reintentar(): queda una sola función, refrescar(), y todos los botones y gestos llaman a esa. El estado uiState ya no se guarda: se calcula cada vez juntando la lista del repositorio con lo que sabe el ViewModel (si está refrescando y el último error). Así la lista tiene una única fuente.
Intenta escribirlo tú: ArbitrosUiState pasa a ser un struct que se puede comparar, con arbitros (vacía por defecto), refrescando (falso por defecto) y error (un texto opcional). En ArbitrosViewModel, refrescando y error son propiedades que se leen desde fuera pero solo cambia el ViewModel; uiState es una propiedad calculada que arma el estado con repositorio.arbitros; y refrescar() (asíncrona) pone refrescando a verdadero y borra el error, intenta repositorio.refrescar(), y si falla guarda error.localizedDescription y lo anota con Registro.anotar; al final pone refrescando a falso.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
import Observation
// LO QUE VE LA PANTALLA DE ÁRBITROS. Ya no es un `enum`: con dos fuentes se pueden dar VARIAS cosas
// a la vez (la lista guardada, la ruedecita de refrescar y un aviso de que la red falló).
struct ArbitrosUiState: Equatable {
var arbitros: [Arbitro] = []
var refrescando = false
var error: String?
}
@MainActor
@Observable
final class ArbitrosViewModel {
private(set) var refrescando = false
private(set) var error: String?
@ObservationIgnored private let repositorio: any ArbitrosRepositorio
init(repositorio: any ArbitrosRepositorio) {
self.repositorio = repositorio
}
// La lista sale del repositorio (lo guardado); el ViewModel solo añade si está refrescando y el aviso.
var uiState: ArbitrosUiState {
ArbitrosUiState(arbitros: repositorio.arbitros, refrescando: refrescando, error: error)
}
// Al abrir la pantalla (`.task`), al tirar hacia abajo (`.refreshable`) y con «Actualizar».
func refrescar() async {
refrescando = true
error = nil
do {
try await repositorio.refrescar()
} catch {
// `ErrorDeRed` es `LocalizedError`: «No hay conexión» o «El servidor respondió…».
self.error = error.localizedDescription
Registro.anotar("árbitros: no se pudieron refrescar (\(error))")
}
refrescando = false
}
}self.error = error.localizedDescription: dentro del catch, error a secas es el error que acaba de saltar; para hablar de la propiedad del ViewModel hay que escribir self.error. Gracias a LocalizedError, que ErrorDeRed ya cumple desde POST y errores de red, ese texto es el «No hay conexión» de siempre.
Una duda razonable: repositorio lleva @ObservationIgnored, y aun así la pantalla se redibuja cuando llega una lista nueva. ¿Cómo? @ObservationIgnored solo dice que el ViewModel no vigila su propia propiedad repositorio (un let, que nunca cambia). Pero cuando la vista lee viewModel.uiState, esa propiedad calculada lee repositorio.arbitros, y el repositorio sí es @Observable: SwiftUI apunta que la vista ha leído arbitros y la redibuja cuando refrescar() lo cambie. Lo que cuenta es qué propiedad observable se lee, no por cuántas capas pasa la lectura.
Ahora la vista. Tiene tres piezas nuevas:
.overlay { … }: dibuja una vista encima de otra sin cambiar su tamaño. Aquí pone la ruedecita o el error encima de la lista cuando no hay nada guardado..toolbar { … }con un botón «Actualizar» (el.disabledlo apaga mientras se refresca)..refreshable { … }, la de arriba: tirar de la lista hacia abajo.
Se usa un overlay y no un switch que cambie de vista porque así la List existe siempre y .refreshable siempre tiene a qué agarrarse. El ContentUnavailableView ya lo conoces de Red en Marcador: la lista de árbitros: sus tres huecos con nombre son el título (Label), la descripción (description:) y las acciones, los botones (actions:).
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import SwiftUI
// La pantalla de los árbitros: pinta lo GUARDADO y, encima, el aviso si la red falló.
struct ArbitrosView: View {
@State private var viewModel: ArbitrosViewModel
init(viewModel: ArbitrosViewModel) {
_viewModel = State(wrappedValue: viewModel)
}
var body: some View {
let estado = viewModel.uiState
List {
if let error = estado.error, !estado.arbitros.isEmpty {
// Hay datos guardados, pero no se pudieron renovar: se avisa sin vaciar la lista.
Label(error, systemImage: "exclamationmark.triangle")
.font(.footnote)
.foregroundStyle(.orange)
}
ForEach(estado.arbitros) { arbitro in
// «Leanne Graham · Gwenborough»
Text("\(arbitro.nombre) · \(arbitro.ciudad)")
}
}
.listStyle(.plain)
// Si no hay nada guardado, en lugar de una lista vacía: la ruedecita o el error.
.overlay {
if estado.arbitros.isEmpty {
if let error = estado.error {
// `ContentUnavailableView`: la pantalla vacía de iOS, con icono, texto y acciones.
ContentUnavailableView {
Label(error, systemImage: "wifi.slash")
} description: {
Text("Comprueba la conexión y vuelve a intentarlo.")
} actions: {
Button("Reintentar") {
Task { await viewModel.refrescar() }
}
.buttonStyle(.borderedProminent)
}
} else {
ProgressView("Buscando árbitros…")
}
}
}
.navigationTitle("Árbitros")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Actualizar") {
Task { await viewModel.refrescar() }
}
.disabled(estado.refrescando)
}
// Tirar de la lista hacia abajo también refresca.
.refreshable { await viewModel.refrescar() }
// Al aparecer: se ve lo guardado al instante y la red refresca por detrás.
.task { await viewModel.refrescar() }
}
}Paso 3 · Conectar: la lista de partidos y el arranque
Objetivo: que la app construya el repositorio nuevo con su almacén, y que -reiniciarDatos YES borre también los árbitros guardados. Con este paso termina el rojo.
Dos ficheros, cambios pequeños. En ListaDePartidosView.swift, el destino .arbitros (dentro de .navigationDestination) crea ahora RepositorioDeArbitros con el almacén de verdad. Sigue construyéndose «a escondidas» ahí mismo; la lección Inyección de dependencias en todas las capas lo arregla.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
case .arbitros:
// OJO: la pantalla crea aquí, a escondidas, el servicio y el repositorio. Funciona,
// pero cada vez que se abre nace un repositorio nuevo. Se arregla con la inyección.
ArbitrosView(viewModel: ArbitrosViewModel(
repositorio: RepositorioDeArbitros(servicio: ServicioJSONPlaceholder(), almacen: .predeterminado)
))Solo cambia la línea del repositorio:; el resto del fichero no cambia. En MarcadorApp.swift, el bloque que crea el repositorio de partidos borra también el fichero de árbitros cuando la app arranca con -reiniciarDatos YES (el argumento de Guardar el torneo en un fichero):
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// El repositorio, creado una vez y repartido a las pantallas por el entorno.
// Con el torneo guardado en `Documents/torneo.json` (`-reiniciarDatos YES` lo borra al arrancar,
// y también los árbitros guardados en `Documents/arbitros.json`).
@State private var repositorio: RepositorioDePartidos = {
if Configuracion.reiniciarDatos {
AlmacenDelTorneo.predeterminado.borrar()
AlmacenDeArbitros.predeterminado.borrar()
UserDefaults.standard.removeObject(forKey: Ajustes.claveDuracion)
}
return RepositorioDePartidos(almacen: .predeterminado)
}()El resto de MarcadorApp.swift no cambia. Compila con Cmd+B: tendrías que ver Build Succeeded en la app, y ya no queda rojo en ella. Si quedan errores en MarcadorAppTests, son los dobles del paso siguiente.
Paso 4 · Las pruebas
Objetivo: probar el repositorio con un servicio falso y un fichero de verdad en una carpeta temporal, y el ViewModel con un repositorio falso.
Primero el doble. En Dobles.swift, el ArbitrosRepositorioFalso de antes (que devolvía una respuesta a la carta) se sustituye por uno que empieza con lo «guardado» que le des y, al refrescar, o cambia su lista o falla con un ErrorDeRed. Como ya no usa URLError, la línea import Foundation de arriba del fichero sobra: bórrala (si más adelante Xcode te dice que falta algo de Foundation, vuelve a ponerla).
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// El repositorio de árbitros falso: empieza con lo «guardado» que le des y, al refrescar,
// devuelve la lista nueva o falla, a la carta.
final class ArbitrosRepositorioFalso: ArbitrosRepositorio {
private(set) var arbitros: [Arbitro]
var alRefrescar: Result<[Arbitro], ErrorDeRed> = .success([])
init(guardados: [Arbitro] = []) {
self.arbitros = guardados
}
func refrescar() async throws {
arbitros = try alRefrescar.get()
}
}alRefrescar es un Result: o .success(lista), o .failure(error). try alRefrescar.get() devuelve la lista si es un éxito y lanza el error si es un fallo; así el falso se comporta como el repositorio de verdad, que o cambia su lista o lanza.
El resto de Dobles.swift no cambia. Después, el ViewModel. ArbitrosViewModelTests.swift se reescribe entero con dos pruebas: la primera comprueba que antes de ir a la red ya se ve lo guardado y que al refrescar se ve lo nuevo; la segunda, que sin red la lista se conserva y aparece el aviso.
Intenta escribir las dos. El Arbitro de ejemplo (Leanne y Ervin) y el estado que esperas en la primera son: antes de refrescar, un estado con solo la lista [leanne]; después de refrescar con éxito, uno con [leanne, ervin]. La segunda se parece a la primera, pero el repositorio falso responde con un fallo (.failure(.sinConexion)) y esperas el mismo estado con [leanne], sin refrescar y con el aviso «No hay conexión».
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Testing
@testable import MarcadorApp
// El ViewModel de los árbitros con un repositorio FALSO: sin red, sin disco y sin servidor.
@MainActor
struct ArbitrosViewModelTests {
private let leanne = Arbitro(id: 1, nombre: "Leanne Graham", ciudad: "Gwenborough")
private let ervin = Arbitro(id: 2, nombre: "Ervin Howell", ciudad: "Wisokyburgh")
@Test func pintaLoGuardadoYAlRefrescarLoNuevo() async {
let repositorio = ArbitrosRepositorioFalso(guardados: [leanne])
repositorio.alRefrescar = .success([leanne, ervin])
let viewModel = ArbitrosViewModel(repositorio: repositorio)
// Antes de ir a la red ya se ve lo guardado.
#expect(viewModel.uiState == ArbitrosUiState(arbitros: [leanne]))
await viewModel.refrescar()
#expect(viewModel.uiState == ArbitrosUiState(arbitros: [leanne, ervin]))
}
@Test func sinRedConservaLaListaYMuestraElAviso() async {
let repositorio = ArbitrosRepositorioFalso(guardados: [leanne])
repositorio.alRefrescar = .failure(.sinConexion)
let viewModel = ArbitrosViewModel(repositorio: repositorio)
await viewModel.refrescar()
#expect(viewModel.uiState == ArbitrosUiState(arbitros: [leanne], refrescando: false, error: "No hay conexión"))
}
}Por último, las pruebas del repositorio: la parte más importante de la lección. Usan un ServicioFalso (el de La pantalla de la crónica y sus pruebas) y un fichero real en una carpeta temporal, para no tocar el arbitros.json de tu app. La primera comprueba que al refrescar se guarda en el disco y que otro repositorio nuevo, sin red, lee lo guardado; la segunda, que sin red se conserva lo guardado y el error es de dominio.
Crea RepositorioDeArbitrosTests.swift en MarcadorAppTests/Arbitros/Data (la carpeta MarcadorAppTests/Arbitros ya la tienes; clic derecho sobre ella → New Folder → Data, y en esa Data New File from Template… → Swift File, solo el target MarcadorAppTests; con la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorAppTests/Arbitros/Data && touch ~/Marcador-Swift/MarcadorApp/MarcadorAppTests/Arbitros/Data/RepositorioDeArbitrosTests.swift).
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Foundation
import Testing
@testable import MarcadorApp
// El repositorio de dos fuentes con un servicio FALSO y un fichero de verdad en una carpeta temporal.
@MainActor
struct RepositorioDeArbitrosTests {
private let leanne = ArbitroDTO(id: 1, name: "Leanne Graham", email: "Sincere@april.biz", address: .init(city: "Gwenborough"))
// Un fichero distinto en cada prueba, en la carpeta temporal (no toca el `arbitros.json` de la app).
private func almacenTemporal() -> AlmacenDeArbitros {
AlmacenDeArbitros(url: FileManager.default.temporaryDirectory.appending(path: "\(UUID().uuidString).json"))
}
@Test func alRefrescarGuardaEnElDiscoYOtroRepositorioLoLeeSinRed() async throws {
let almacen = almacenTemporal()
defer { almacen.borrar() }
let servicio = ServicioFalso()
servicio.arbitrosQueDevuelve = .success([leanne])
let repositorio = RepositorioDeArbitros(servicio: servicio, almacen: almacen)
#expect(repositorio.arbitros.isEmpty) // aún no se bajó nada
try await repositorio.refrescar()
#expect(repositorio.arbitros == [Arbitro(id: 1, nombre: "Leanne Graham", ciudad: "Gwenborough")])
// Otro repositorio (como al volver a abrir la app), sin red: lee lo guardado al crearse.
let sinRed = ServicioFalso()
sinRed.arbitrosQueDevuelve = .failure(.sinConexion)
let otro = RepositorioDeArbitros(servicio: sinRed, almacen: almacen)
#expect(otro.arbitros.map(\.nombre) == ["Leanne Graham"])
}
@Test func sinRedConservaLoGuardadoYElErrorEsDeDominio() async throws {
let almacen = almacenTemporal()
defer { almacen.borrar() }
try almacen.guardar([Arbitro(id: 1, nombre: "Leanne Graham", ciudad: "Gwenborough")])
let servicio = ServicioFalso()
servicio.arbitrosQueDevuelve = .failure(.sinConexion)
let repositorio = RepositorioDeArbitros(servicio: servicio, almacen: almacen)
await #expect(throws: ErrorDeRed.sinConexion) {
try await repositorio.refrescar()
}
#expect(repositorio.arbitros.map(\.nombre) == ["Leanne Graham"]) // la lista no se vació
}
}Tres piezas del fichero que quizá no conozcas:
FileManager.default.temporaryDirectoryes la carpeta temporal del sistema, donde se puede escribir sin molestar.UUID().uuidStringes un texto aleatorio único; así cada prueba escribe en un fichero con nombre distinto y nunca pisa al de otra ni alarbitros.jsonde la app.defer { almacen.borrar() }guarda ese bloque para cuando la función termine, sea cual sea el motivo (también si la prueba falla a medias): borra el fichero temporal pase lo que pase.address: .init(city: "Gwenborough")es una abreviatura: Swift sabe por el parámetroaddressqué tipo hay que crear (la dirección del DTO), así que basta con.init(…)sin escribir el nombre del tipo.
Ejecuta con Cmd+U. Salen 48 pruebas (con tus ejercicios de pruebas, 51; sin ellos, 48): 32 unitarias (28 de Swift Testing y 4 de XCTest) y 16 de interfaz. Dos más que antes: las dos del repositorio; las del ViewModel se reescribieron pero siguen siendo dos. Si tienes ejercicios con pruebas propias en el proyecto, como la taquilla, esas suman tres más: no es un error.
Míralo funcionar
-
Arranca la app en el simulador (Cmd+R). Pulsa Árbitros: primero ves la ruedecita «Buscando árbitros…» y después los 10, de «Leanne Graham · Gwenborough» a «Clementina DuBuque · Lebsackbury». Arriba a la derecha tienes Actualizar.
-
Vuelve a la lista de partidos. En la consola de Xcode verás la línea
árbitros REFRESCADOS: 10. -
Sin internet. El simulador usa la red de tu Mac, así que quítasela al Mac: apaga el wifi desde el icono de la barra de menús de macOS (si tu Mac va por cable, desconéctalo). Después, en Xcode, detén la app (Cmd+.) y vuelve a ejecutarla con Cmd+R. Tu esquema de Xcode no lleva
-reiniciarDatos, así que no se borra nada. Abre Árbitros: los 10 aparecen al instante, con el aviso naranja «No hay conexión» encima. Eso es que el disco manda. -
Vuelve a encender el wifi y pulsa Actualizar: el aviso desaparece. Tirar de la lista hacia abajo hace lo mismo.
-
Mira el fichero. Con el simulador arrancado, lista el contenedor de datos de tu app, como hiciste en Guardar el torneo en un fichero:
cd "$(xcrun simctl get_app_container booted com.sebastiannarvaez.marcador data)" ls DocumentsVerás
arbitros.json. Verás tambiéntorneo.jsonsolo si has marcado algún gol a mano desde el último reinicio de datos. (com.sebastiannarvaez.marcadores el identificador de la app; si el tuyo es otro, míralo en Xcode: el proyecto, el targetMarcadorApp, pestaña Signing & Capabilities, campo Bundle Identifier.) -
Empezar de cero. Con el wifi aún apagado, lanza la app con el argumento que borra lo guardado y abre Árbitros:
xcrun simctl launch --terminate-running-process booted com.sebastiannarvaez.marcador -reiniciarDatos YES--terminate-running-processcierra la app si estaba abierta y la abre de nuevo: sin esa opción, si la app ya estaba abierta, el comando solo la trae al frente y el argumento no llega. Ahora no hay nada guardado, y sale la pantalla vacía de iOS con «No hay conexión» y el botón Reintentar. Enciende el wifi, pulsa Reintentar y vuelven los 10.
Punto de control
RepositorioDeArbitrosEnRed.swiftya no está,ArbitrosRepositoriopidearbitrosyrefrescar(), yRepositorioDeArbitroslo cumple.- Cmd+B termina en
Build Succeeded, y Cmd+U da 48 pruebas en verde (con tus ejercicios de pruebas, 51; sin ellos, 48): 32 unitarias y 16 de interfaz. Los ejercicios suman pruebas propias, por eso tu cifra puede ser mayor. - La pantalla de árbitros enseña los 10 al abrirse; sin wifi y con árbitros guardados, los 10 más un aviso naranja «No hay conexión»; sin wifi y sin nada guardado, «No hay conexión» con Reintentar.
- En el contenedor de la app del simulador,
ls Documentsmuestraarbitros.json(ytorneo.jsonsi has marcado algún gol a mano desde el último reinicio de datos). - Sabes decir por qué
ArbitrosUiStatees ahora unstructy no unenum.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Por qué
ArbitrosUiStatepasa deenumastruct?2.¿Qué hace
.refreshable { await viewModel.refrescar() }en una lista?3.La pantalla pone la ruedecita o el error en un
.overlaysobre laListy no cambia de vista con unswitch. ¿Por qué?4.
repositoriolleva@ObservationIgnoredenArbitrosViewModel, y aun así la pantalla se redibuja cuando llega una lista nueva. ¿Por qué?5.Las pruebas del repositorio escriben en un fichero de la carpeta temporal con un nombre distinto en cada prueba. ¿Para qué sirve eso?