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

Red en Marcador: la lista de árbitros

Al terminar: Cargar los árbitros desde internet y pintarlos con los estados de carga, éxito y error.

Al terminar, Marcador tendrá un botón «Árbitros» en la barra de la lista de Partidos. Al tocarlo se abre una pantalla que pide a internet los diez árbitros de la liga, enseña una ruedecita mientras espera y, si algo falla, un mensaje «No hay conexión» con un botón «Reintentar». Es la primera vez que el estado «cargando» de tu ViewModel es de verdad: antes se ponía y se quitaba al instante, porque los datos estaban dentro de la app.

Importa porque es el patrón de casi cualquier pantalla que depende de un servidor: tres estados (cargando, éxito, error), un repositorio que trae los datos y un ViewModel que decide qué se ve. Lo ya conocido de MVVM se aplica sin cambios; lo nuevo es lo que la red trae consigo.

Temas: un repositorio cuya única fuente es la red; el estado cargando / error / éxito con una espera real; ContentUnavailableView; un botón en la barra con .toolbar; y probar el error sin conexión. Nivel 2: usar.

Un repositorio que solo tiene la red

Para el ViewModel, el repositorio es la puerta a los datos. Esta vez detrás de la puerta no hay una lista en memoria, sino el servicio de red. El repositorio hace dos cosas: pedir los DTO al servicio y convertirlos en modelos. Así el ViewModel y la pantalla solo conocen Arbitro, con nombres en español, y nunca el DTO.

Con una sola fuente, sin red no hay datos: la lista queda vacía. En Dos fuentes: el almacén y el repositorio lo arreglarás guardando en el disco; hoy toca ver la pantalla fallar para saber manejar el fallo.

El estado: cargando, error, éxito

El estado de la pantalla es un enum con tres casos, UNO a la vez, como el de MVVM. Lo nuevo es que ahora cargando dura de verdad y error ocurre de verdad. Un ejemplo con una biblioteca, en el que el ViewModel pide libros a un repositorio de mentira que puede fallar:

ejemplo mínimo: un ViewModel que pasa de cargando a éxito o error (con comentarios)
import Foundation
 
// Una biblioteca que presta libros.
// `CustomStringConvertible` (de Protocolos y extensiones) tiene un solo requisito, `description`:
// el texto que `print` escribe para el valor. Aquí solo sirve para que la salida sea corta.
struct Libro: Equatable, CustomStringConvertible {
    let titulo: String
 
    var description: String { titulo }
}
 
// Los tres estados posibles de la pantalla: UNO cada vez.
enum EstadoDeLibros: Equatable {
    case cargando
    case error(String)
    case exito([Libro])
}
 
// El contrato: quien pide libros no sabe si vienen de internet o de un cajón.
protocol LibrosRepositorio {
    func libros() async throws -> [Libro]
}
 
// Un repositorio de mentira: devuelve dos libros o falla, según le digas.
struct LibrosDeMentira: LibrosRepositorio {
    let falla: Bool
 
    struct Fallo: Error {}
 
    func libros() async throws -> [Libro] {
        if falla { throw Fallo() }
        return [Libro(titulo: "Dune"), Libro(titulo: "Ubik")]
    }
}
 
final class LibrosViewModel {
    private(set) var estado: EstadoDeLibros = .cargando     // empieza cargando
    private let repositorio: any LibrosRepositorio          // any: cualquier cosa que cumpla el contrato
 
    init(repositorio: any LibrosRepositorio) {
        self.repositorio = repositorio
    }
 
    func cargar() async {
        estado = .cargando
        do {
            // Si el repositorio lanza un error, salta directo al `catch`.
            estado = .exito(try await repositorio.libros())
        } catch {
            estado = .error("No se pudieron cargar")
        }
    }
}
 
let bueno = LibrosViewModel(repositorio: LibrosDeMentira(falla: false))
print(bueno.estado)         // todavía no ha cargado nada
await bueno.cargar()
print(bueno.estado)
 
let malo = LibrosViewModel(repositorio: LibrosDeMentira(falla: true))
await malo.cargar()
print(malo.estado)
cargando
exito([Dune, Ubik])
error("No se pudieron cargar")

Para probarlo no hace falta internet: el repositorio de mentira es el mismo truco de los falsos de Tests en iOS: falsos y espías. any LibrosRepositorio es el any de some y any, y CustomStringConvertible el de Protocolos y extensiones. El enum del estado es exhaustivo: el switch que lo lee tiene que cubrir los tres casos o no compila, así que no puedes olvidar pintar uno.

Tres piezas de pantalla nuevas

La vista de los árbitros es como la de La pantalla pinta el uiState (un switch sobre el estado), con tres piezas que no habías usado:

  • ProgressView("Buscando árbitros…"): la ruedecita que ya conoces, con un texto debajo.
  • ContentUnavailableView: la pantalla vacía de iOS (desde iOS 17), con icono, título, descripción y botones. Se usa cuando no hay nada que enseñar: sin conexión, sin resultados… Su forma larga lleva tres bloques entre llaves, cada uno con su etiqueta: el primero, sin etiqueta, es el título con icono (Label); description: es el texto de ayuda y actions: son los botones. Es la sintaxis de los cierres que ya conoces, repetida con nombres.
  • .buttonStyle(.borderedProminent) (aparece en la vista): un botón con el fondo de color, para el botón principal de la pantalla.
  • .toolbar { … }: la barra de arriba. Lo que pones dentro aparece en ella; un NavigationLink(value:) allí es un botón que apila una pantalla, igual que una fila de la lista.

Reto · Ejercicio: el estado de una pantalla de recetas

En tu Playground (reutiliza el mismo y borra lo anterior; ahí await va suelto, como en Red: HTTP, JSON y URLSession), escribe lo necesario para que una pantalla de recetas pase de cargando a éxito o a error:

  1. Una receta con nombre y minutos, que se pueda comparar y que al imprimirse salga como «Tortilla (20 min)» (con CustomStringConvertible, como el Libro del ejemplo).
  2. Un estado con tres casos: cargando, error (con un mensaje) y éxito (con la lista de recetas).
  3. Un contrato, RecetasRepositorio, con una función asíncrona que puede fallar y devuelve las recetas.
  4. Un repositorio de mentira que, según se le diga al crearlo, devuelve dos recetas, Tortilla (20 min) y Gazpacho (15 min), o falla con un error propio.
  5. Un RecetasViewModel que empieza en cargando, tiene una función cargar y, si el repositorio falla, deja el estado en error con el mensaje «No hay conexión».
  6. Crea un ViewModel con el repositorio que funciona e imprime su estado antes y después de cargar; crea otro con el que falla e imprime también su estado antes y después de cargar.

Lo que debe salir:

cargando
exito([Tortilla (20 min), Gazpacho (15 min)])
cargando
error("No hay conexión")
Ver solución
solución: el estado de una pantalla de recetas
struct Receta: Equatable, CustomStringConvertible {
    let nombre: String
    let minutos: Int
 
    var description: String { "\(nombre) (\(minutos) min)" }
}
 
enum EstadoDeRecetas: Equatable {
    case cargando
    case error(String)
    case exito([Receta])
}
 
protocol RecetasRepositorio {
    func recetas() async throws -> [Receta]
}
 
struct RecetasDeMentira: RecetasRepositorio {
    let falla: Bool
 
    struct SinRed: Error {}
 
    func recetas() async throws -> [Receta] {
        if falla { throw SinRed() }
        return [Receta(nombre: "Tortilla", minutos: 20), Receta(nombre: "Gazpacho", minutos: 15)]
    }
}
 
final class RecetasViewModel {
    private(set) var estado: EstadoDeRecetas = .cargando
    private let repositorio: any RecetasRepositorio
 
    init(repositorio: any RecetasRepositorio) {
        self.repositorio = repositorio
    }
 
    func cargar() async {
        estado = .cargando
        do {
            estado = .exito(try await repositorio.recetas())
        } catch {
            estado = .error("No hay conexión")
        }
    }
}
 
let conRed = RecetasViewModel(repositorio: RecetasDeMentira(falla: false))
print(conRed.estado)
await conRed.cargar()
print(conRed.estado)
 
let sinRed = RecetasViewModel(repositorio: RecetasDeMentira(falla: true))
print(sinRed.estado)
await sinRed.cargar()
print(sinRed.estado)

El estado de cada ViewModel empieza en cargando antes de llamar a cargar(); por eso cargando sale dos veces, una por cada ViewModel. Cada catch convierte el fallo del repositorio en un mensaje que la pantalla puede enseñar tal cual.

Paso 1 · El modelo y el contrato: Arbitro.swift

Objetivo: el tipo que pintará la pantalla, con su traducción desde el DTO, y el contrato del repositorio.

MarcadorApp (proyecto)
MarcadorApp
Arbitros
Domain
Arbitro.swiftse crea
Data
RepositorioDeArbitrosEnRed.swiftse crea
UI
ArbitrosViewModel.swiftse crea
ArbitrosView.swiftse crea
Destino.swiftse modifica
Partidos
UI
ListaDePartidosView.swiftse modifica
MarcadorAppTests
Core
Dobles.swiftse modifica
Arbitros
UI
ArbitrosViewModelTests.swiftse crea
Los ficheros de esta lección en el Project navigator.

Los árbitros son una funcionalidad nueva y cada capa va en su carpeta: Domain (el modelo), Data (de dónde salen los datos) y UI (la pantalla). Clic derecho sobre la carpeta MarcadorApp (la de dentro, donde está MarcadorApp.swift) → New Folder → Arbitros; luego, clic derecho sobre Arbitros → New Folder → Domain, y lo mismo para Data y UI. Las otras dos las usarás un poco más abajo. Crea Arbitro.swift en Domain: clic derecho sobre Domain → New File from Template… → Swift File. Con la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorApp/Arbitros/{Domain,Data,UI} && touch ~/Marcador-Swift/MarcadorApp/MarcadorApp/Arbitros/Domain/Arbitro.swift. El protocolo del repositorio va en ese mismo fichero y por eso vive en Domain; la clase que lo cumple irá en Data.

Dos ideas antes de escribir.

  • El init(dto:) va en una extension, fuera del struct. Un struct recibe gratis un init con todos sus campos (Arbitro(id:nombre:ciudad:)). Si escribes tu propio init dentro del struct, Swift deja de dar el gratuito; en una extension conservas los dos. Las pruebas de más abajo llaman al init de los tres campos, y por eso el tuyo va fuera (Protocolos y extensiones cuenta las extensiones).
  • : AnyObject en el protocolo quiere decir «solo lo cumplen clases» (no struct): el mismo AnyObject de Delegados y tareas. Lo pedimos porque el falso de las pruebas será una clase cuyo contenido cambia mientras el ViewModel lo tiene, y los dos tienen que compartir la misma instancia.

Intenta escribirlo tú: Arbitro, con número, nombre y ciudad, que se identifica por su número y se puede comparar. Una extensión con un init(dto:) que lo construye a partir de un ArbitroDTO (la ciudad está dentro de la dirección). Y ArbitrosRepositorio, un protocolo solo para clases, con una función asíncrona que puede fallar, arbitros(), que devuelve una lista de Arbitro.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Arbitros/Domain/Arbitro.swift
// 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.
struct Arbitro: Identifiable, 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: la puerta por la que el ViewModel los pide, sin saber de dónde salen.
protocol ArbitrosRepositorio: AnyObject {
    func arbitros() async throws -> [Arbitro]
}

Paso 2 · El repositorio de red: RepositorioDeArbitrosEnRed.swift

Objetivo: el repositorio cuya única fuente es el servicio de red. Va en Arbitros/Data: es de donde salen los datos.

Antes de escribir. Pasar de DTO a modelo es un map: lista.map { Arbitro(dto: $0) } crea un Arbitro por cada elemento (el $0 de Closures con listas). Cuando el cierre solo llama a una función con su argumento, se puede dar el nombre de la función en vez del cierre: lista.map(Arbitro.init(dto:)) es lo mismo, más corto. Arbitro.init(dto:) es el init visto como una función.

Intenta escribirlo tú: RepositorioDeArbitrosEnRed, una clase final (que no se hereda), cumple el contrato, recibe en su init un servicio (cualquier cosa que cumpla ServicioDelTorneo) y, en arbitros(), pide los DTO al servicio y los convierte en Arbitro con map. Si el servicio lanza un error, lo deja pasar.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Arbitros/Data/RepositorioDeArbitrosEnRed.swift
// Un repositorio cuya ÚNICA fuente es la red: pide los DTO al servicio y los convierte en modelos.
// Sin red no hay árbitros (más adelante se guardarán en el disco).
final class RepositorioDeArbitrosEnRed: ArbitrosRepositorio {
    private let servicio: any ServicioDelTorneo
 
    init(servicio: any ServicioDelTorneo) {
        self.servicio = servicio
    }
 
    func arbitros() async throws -> [Arbitro] {
        try await servicio.arbitros().map(Arbitro.init(dto:))
    }
}

map(Arbitro.init(dto:)) aplica la traducción a cada elemento de la lista (lo mismo que map { Arbitro(dto: $0) }). try await espera la respuesta y deja pasar el error: no se atrapa aquí, porque quien decide qué hacer con él es el ViewModel.

Paso 3 · El ViewModel: ArbitrosViewModel.swift

Objetivo: el enum de estado y el ViewModel que lo gobierna. Va en Arbitros/UI, con la pantalla que lo lee.

Intenta escribirlo tú: ArbitrosUiState, un enum que se puede comparar, con cargando, error (con un texto) y exito (con la lista de árbitros). ArbitrosViewModel, una clase final con @MainActor y @Observable, como PartidoViewModel, con un uiState que solo él cambia (empieza en cargando), un repositorio recibido por el init, una función cargar() y una función reintentar() que vuelve a cargar. Si el repositorio falla, cargar() anota el fallo con Registro.anotar (el de La app, sus escenas y el paquete Torneo) y pone el estado en error con «No hay conexión».

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Arbitros/UI/ArbitrosViewModel.swift
import Foundation
import Observation
 
// LO QUE VE LA PANTALLA DE ÁRBITROS. Esta vez «cargando» es de verdad: la red tarda.
enum ArbitrosUiState: Equatable {
    case cargando
    case error(String)
    case exito([Arbitro])
}
 
@MainActor
@Observable
final class ArbitrosViewModel {
    private(set) var uiState: ArbitrosUiState = .cargando
 
    @ObservationIgnored private let repositorio: any ArbitrosRepositorio
 
    init(repositorio: any ArbitrosRepositorio) {
        self.repositorio = repositorio
    }
 
    // La lanza la vista con `.task` al aparecer.
    func cargar() async {
        uiState = .cargando
        do {
            let arbitros = try await repositorio.arbitros()
            uiState = .exito(arbitros)
        } catch {
            Registro.anotar("árbitros: no se pudieron cargar (\(error))")
            uiState = .error("No hay conexión")
        }
    }
 
    // El botón «Reintentar»: vuelve a pedirlos.
    func reintentar() async {
        await cargar()
    }
}

Paso 4 · La pantalla: ArbitrosView.swift

Objetivo: la vista que pinta cada estado. Va en Arbitros/UI, junto a su ViewModel.

Intenta escribirla tú: ArbitrosView recibe su ViewModel en el init y lo guarda en un @State (con State(wrappedValue:), como en La pantalla pinta el uiState). Su cuerpo es un switch sobre el uiState: ProgressView("Buscando árbitros…") si carga; un ContentUnavailableView con el mensaje, el icono wifi.slash, la descripción «Comprueba la conexión y vuelve a intentarlo.» y un botón «Reintentar» si hay error; y una List con «nombre · ciudad» por árbitro si hay éxito. Lleva el título «Árbitros» en la barra y lanza cargar() con .task. Un detalle: un body tiene que devolver una vista, y un switch suelto no lo es; por eso va envuelto en un Group { … }, que agrupa lo que haya dentro en una sola vista para que el título y el .task de debajo se apliquen al estado que toque.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Arbitros/UI/ArbitrosView.swift
import SwiftUI
 
// La pantalla de los árbitros: pinta el `uiState` del ViewModel, como la del partido.
struct ArbitrosView: View {
    @State private var viewModel: ArbitrosViewModel
 
    init(viewModel: ArbitrosViewModel) {
        _viewModel = State(wrappedValue: viewModel)
    }
 
    var body: some View {
        Group {
            switch viewModel.uiState {
            case .cargando:
                ProgressView("Buscando árbitros…")
            case let .error(mensaje):
                // `ContentUnavailableView`: la pantalla vacía de iOS, con icono, texto y acciones.
                ContentUnavailableView {
                    Label(mensaje, systemImage: "wifi.slash")
                } description: {
                    Text("Comprueba la conexión y vuelve a intentarlo.")
                } actions: {
                    Button("Reintentar") {
                        Task { await viewModel.reintentar() }
                    }
                    .buttonStyle(.borderedProminent)
                }
            case let .exito(arbitros):
                List(arbitros) { arbitro in
                    // «Leanne Graham · Gwenborough»
                    Text("\(arbitro.nombre) · \(arbitro.ciudad)")
                }
                .listStyle(.plain)
            }
        }
        .navigationTitle("Árbitros")
        .navigationBarTitleDisplayMode(.inline)
        // Arranca al aparecer la pantalla y se cancela si se va antes de terminar.
        .task { await viewModel.cargar() }
    }
}

Task { await viewModel.reintentar() } crea una tarea desde un botón, que no es async; es la misma Task { … } de async/await y Task. Y .navigationBarTitleDisplayMode(.inline) deja el título pequeño en el centro de la barra.

Paso 5 · El botón «Árbitros» en la lista

Objetivo: que desde la lista de Partidos se llegue a la pantalla.

Dos cambios pequeños. El primero, un caso nuevo en Destino.swift, el fichero de NavigationStack:

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Destino.swift
enum Destino: Hashable {
    case partido(Int)   // el id del partido (viaja en la clave, en un solo sitio)
    case goleadores
    case arbitros       // la lista de árbitros (botón «Árbitros» de la barra de Partidos)
}

El resto del fichero no cambia: solo se añade la línea de arbitros.

El segundo, en ListaDePartidosView.swift: un botón en la barra y el destino que construye la pantalla. Dentro del NavigationStack, sustituye lo que hay desde .listStyle(.plain) hasta el cierre de .navigationDestination por este bloque (añade el .toolbar y el caso .arbitros):

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorApp/Partidos/UI/ListaDePartidosView.swift
            .listStyle(.plain)
            .navigationTitle("Partidos")
            // Un botón en la barra de arriba que empuja la pantalla de árbitros.
            .toolbar {
                NavigationLink("Árbitros", value: Destino.arbitros)
            }
            // UN solo sitio decide qué pantalla corresponde a cada destino.
            .navigationDestination(for: Destino.self) { destino in
                switch destino {
                case let .partido(id):
                    PartidoContenedor(id: id)
                case .goleadores:
                    GoleadoresView()
                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: RepositorioDeArbitrosEnRed(servicio: ServicioJSONPlaceholder())
                    ))
                }
            }

El resto del fichero no cambia.

Fíjate en el comentario «a escondidas»: la lista crea ella misma el servicio, el repositorio y el ViewModel. Funciona, pero cada vez que abres la pantalla nacen piezas nuevas y no hay forma de cambiarlas en las pruebas. Es un problema a propósito: se arregla en Inyección de dependencias en todas las capas.

Ejecuta con Cmd+R y toca «Árbitros» arriba a la derecha: tras la ruedecita, diez filas, de «Leanne Graham · Gwenborough» a «Clementina DuBuque · Lebsackbury».

Paso 6 · Las pruebas

Objetivo: probar el ViewModel sin red, con un repositorio falso.

Primero, el falso. En Dobles.swift (el de Tests en iOS: falsos y espías, en MarcadorAppTests/Core) añade import Foundation arriba del todo, porque el falso usa URLError, y la clase nueva al final:

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorAppTests/Core/Dobles.swift
import Foundation
import Torneo
@testable import MarcadorApp
 
// … lo que ya tenías no cambia …
 
// El repositorio de árbitros falso: devuelve lo que le digas (una lista o un fallo) y cuenta las llamadas.
final class ArbitrosRepositorioFalso: ArbitrosRepositorio {
    var respuesta: Result<[Arbitro], URLError>
    private(set) var llamadas = 0
 
    init(respuesta: Result<[Arbitro], URLError>) {
        self.respuesta = respuesta
    }
 
    func arbitros() async throws -> [Arbitro] {
        llamadas += 1
        return try respuesta.get()
    }
}

Result<[Arbitro], URLError> es el Result de MVVM: el estado, el repositorio y el caso de uso: guarda una lista (.success) o un fallo (.failure), y try respuesta.get() devuelve la lista o lanza el fallo. Después, el fichero de pruebas, ArbitrosViewModelTests.swift. Las pruebas copian las carpetas de la app, y este ViewModel está en Arbitros/UI: clic derecho sobre MarcadorAppTests → New Folder → Arbitros, y dentro otra New Folder → UI; después, clic derecho sobre UI → New File from Template… → Swift File, con solo el target MarcadorAppTests marcado. Con la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorAppTests/Arbitros/UI && touch ~/Marcador-Swift/MarcadorApp/MarcadorAppTests/Arbitros/UI/ArbitrosViewModelTests.swift.

Intenta escribirlas tú, en una estructura @MainActor con Swift Testing:

  1. pasaDeCargandoAExito: con un repositorio falso que devuelve a Leanne Graham, el estado empieza en cargando y, tras cargar(), es éxito con ella.
  2. sinRedSaleElErrorYReintentarVuelveACargar: con un repositorio falso que falla con URLError(.notConnectedToInternet), tras cargar() el estado es error con «No hay conexión»; después el falso pasa a devolver a Leanne, se llama a reintentar() y el estado es éxito, con el repositorio llamado dos veces.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorAppTests/Arbitros/UI/ArbitrosViewModelTests.swift
import Foundation
import Testing
@testable import MarcadorApp
 
// El ViewModel de los árbitros con un repositorio FALSO: sin red, sin esperas y sin servidor.
@MainActor
struct ArbitrosViewModelTests {
    private let leanne = Arbitro(id: 1, nombre: "Leanne Graham", ciudad: "Gwenborough")
 
    @Test func pasaDeCargandoAExito() async {
        let repositorio = ArbitrosRepositorioFalso(respuesta: .success([leanne]))
        let viewModel = ArbitrosViewModel(repositorio: repositorio)
        #expect(viewModel.uiState == .cargando)
        await viewModel.cargar()
        #expect(viewModel.uiState == .exito([leanne]))
    }
 
    @Test func sinRedSaleElErrorYReintentarVuelveACargar() async {
        let repositorio = ArbitrosRepositorioFalso(respuesta: .failure(URLError(.notConnectedToInternet)))
        let viewModel = ArbitrosViewModel(repositorio: repositorio)
        await viewModel.cargar()
        #expect(viewModel.uiState == .error("No hay conexión"))
        // Vuelve la red: «Reintentar» pide otra vez la lista.
        repositorio.respuesta = .success([leanne])
        await viewModel.reintentar()
        #expect(viewModel.uiState == .exito([leanne]))
        #expect(repositorio.llamadas == 2)
    }
}

Lánzalas con Cmd+U.

Punto de control

  • Cmd+B termina en Build Succeeded, y Cmd+U da 43 pruebas en verde: 27 unitarias y 16 de interfaz. Las dos nuevas son pasaDeCargandoAExito y sinRedSaleElErrorYReintentarVuelveACargar. Si dejaste el ejercicio de la taquilla en MarcadorAppTests/Ejercicios, salen 46: los ejercicios con pruebas suman a la cuenta.
  • En el simulador, el botón «Árbitros» aparece arriba a la derecha de la lista de Partidos y abre la pantalla con 10 filas, la primera «Leanne Graham · Gwenborough».
  • Con el wifi del Mac apagado, la pantalla enseña «No hay conexión» y el botón «Reintentar»; con el wifi encendido otra vez, «Reintentar» trae la lista.
  • El ejercicio de las recetas del Playground imprime las cuatro líneas.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué hace RepositorioDeArbitrosEnRed con lo que le devuelve el servicio?

  2. 2.¿Por qué el estado de la pantalla es un enum con cargando, error y exito y no tres Bool?

  3. 3.El botón «Reintentar» llama a Task { await viewModel.reintentar() }. ¿Por qué hace falta Task?

  4. 4.En las pruebas de ArbitrosViewModel no se toca internet. ¿Cómo se consigue?

  5. 5.La lista de Partidos crea el servicio y el repositorio dentro del case .arbitros. ¿Qué problema tiene?

Faltan 5 de 5