Equipos y plantillas en iOS
Al terminar: Ver los equipos y su plantilla en pantalla y guardar un jugador nuevo en el fichero del torneo, sin romper los partidos.
Al terminar, la barra de «Partidos» de Marcador tendrá un segundo botón, «Equipos», junto a «Árbitros». Abre la lista de los cuatro equipos, cada uno con su número de jugadores, y tocar uno enseña su plantilla: un nombre por fila y su dorsal a la derecha. Además, el repositorio sabrá añadir un jugador a un equipo, guardarlo en el fichero del torneo y dejarlo disponible para marcar goles. Todavía no hay pantalla para añadirlo: lo comprobarás con una prueba, y el formulario llega en las dos lecciones siguientes.
Importa porque hasta hoy los equipos eran un dato fijo, y un Equipo de Swift no se puede cambiar: sus propiedades son let. Añadir un jugador exige construir otro equipo y ponerlo en el sitio del viejo, en la lista de equipos y en cada partido donde juega. Si te dejas un sitio, el torneo se contradice, y lo verás con tus ojos en un ejemplo. Temas: sustituir un valor inmutable, comparar identidad con ===, por qué en iOS no hace falta ninguna migración, LabeledContent, dos enlaces en la barra y una private extension. Nivel 2: usar.
Un valor que no se puede cambiar se sustituye
Equipo tiene un nombre y una plantilla, los dos let. Es a propósito: un equipo se construye con datos que valen (el init? rechaza un dorsal repetido) y después nadie los toca. Pero ahora hay que añadir un jugador, y eso es cambiar la plantilla.
La salida es construir otro equipo con la plantilla vieja más el jugador nuevo, y ponerlo donde estaba el viejo. «Donde estaba» son todos los sitios que guardaban una referencia al equipo viejo: la lista de equipos, el local y el visitante de sus partidos y los goles que marcó, porque un gol guarda el equipo que lo marcó.
Es como una lista de invitados impresa y pegada en tres puertas: para añadir un nombre imprimes otra y cambias la vieja en cada puerta. Si olvidas una, en esa sigue la de antes, y el código compila igual. Por eso el ejemplo y la prueba del paso 3 comprueban cada sitio.
El ejemplo usa una biblioteca. Un Autor no se puede cambiar, y cada Libro apunta a su autor. Para «añadir una obra» se construye otro Autor y se sustituye en la lista de autores y en cada libro que apuntaba al viejo. Dos piezas hacen el trabajo: map (de Closures con listas) recorre la lista y devuelve otra, y === decide cuál es el elemento a sustituir. Una más pequeña: borges.obras + ["El libro de arena"] suma dos listas con + y da una lista nueva con las obras de antes y la nueva al final.
// Un autor no se puede cambiar (sus propiedades son `let`), y cada libro apunta a SU autor.
final class Autor {
let nombre: String
let obras: [String]
init(nombre: String, obras: [String]) {
self.nombre = nombre
self.obras = obras
}
}
struct Libro {
let titulo: String
let autor: Autor
}
let borges = Autor(nombre: "Borges", obras: ["Ficciones"])
let cortazar = Autor(nombre: "Cortázar", obras: ["Rayuela"])
var autores = [borges, cortazar]
var libros = [
Libro(titulo: "Ficciones", autor: borges),
Libro(titulo: "El Aleph", autor: borges),
Libro(titulo: "Rayuela", autor: cortazar),
]
// «Añadir una obra» = construir OTRO autor con una obra más.
let borgesNuevo = Autor(nombre: "Borges", obras: borges.obras + ["El libro de arena"])
print("mismo nombre:", borges.nombre == borgesNuevo.nombre)
print("mismo objeto:", borges === borgesNuevo)
// Se pone en el sitio del viejo: en la lista de autores...
autores = autores.map { $0 === borges ? borgesNuevo : $0 }
// ...y en cada libro que apuntaba al viejo.
print("antes de sustituir en los libros:", libros[0].autor === borgesNuevo)
libros = libros.map { $0.autor === borges ? Libro(titulo: $0.titulo, autor: borgesNuevo) : $0 }
print("después de sustituir en los libros:", libros[0].autor === borgesNuevo)
print("Rayuela sigue con su autor de antes:", libros[2].autor === cortazar)
print("obras de Borges:", autores[0].obras.count)mismo nombre: true
mismo objeto: false
antes de sustituir en los libros: false
después de sustituir en los libros: true
Rayuela sigue con su autor de antes: true
obras de Borges: 2Léelo línea a línea. $0 === borges ? borgesNuevo : $0 quiere decir: «si este elemento es el autor viejo, pon el nuevo; si no, déjalo como está». Los dos Borges tienen el mismo nombre y, aun así, no son el mismo objeto: por eso la tercera línea sale false hasta que sustituyes en los libros. Y Cortázar no se toca: map solo reemplaza lo que coincide.
En iOS no hace falta migración
En Android, una plantilla guardada era una tabla nueva y subir la base a la versión 4: una migración (el paso que crea la tabla en los móviles que ya tenían la app). En iOS no hay nada que migrar. El torneo se guarda como JSON, y ese JSON ya incluía la plantilla de cada equipo desde Codable: guardar y cargar el torneo. Añadir un jugador cambia lo que hay dentro del fichero, no su forma. Lo guardado antes se sigue leyendo igual.
Reto · Ejercicio: una orquesta y sus conciertos
En tu Playground (reutiliza el mismo y borra lo anterior), escribe una clase final, Seccion, con un nombre y una lista de músicos (los dos let), y un struct Concierto con un título y una sección. Después, una función que reciba el nombre de un músico, la sección vieja y la lista de conciertos, y devuelva la lista con la sección nueva (la vieja más ese músico) en los conciertos que tenían la vieja. Los demás conciertos no se tocan. Construye la sección nueva una sola vez, antes de recorrer la lista, y repártela: así los conciertos que la reciben comparten el mismo objeto.
Pruébala con dos secciones, «Cuerdas» (Ana y Luis) y «Vientos» (Marta), y tres conciertos: «Apertura» (Cuerdas), «Gala» (Vientos) y «Cierre» (Cuerdas). Incorpora a Bea a las Cuerdas. Imprime, por cada concierto, su título, la sección y sus músicos, y después tres comprobaciones con ===. Para unir los músicos con comas usa lista.joined(separator: ", "): junta una lista de textos en uno solo. Lo que debe salir:
Apertura: Cuerdas (Ana, Luis, Bea)
Gala: Vientos (Marta)
Cierre: Cuerdas (Ana, Luis, Bea)
Apertura y Cierre comparten sección: true
Gala conserva su sección de antes: true
La sección de antes no cambió: 2La última línea es cuántos músicos tiene aún la sección cuerdas original.
Ver solución
final class Seccion {
let nombre: String
let musicos: [String]
init(nombre: String, musicos: [String]) {
self.nombre = nombre
self.musicos = musicos
}
}
struct Concierto {
let titulo: String
let seccion: Seccion
}
// Devuelve los conciertos con la sección nueva donde estaba la vieja.
func incorporar(_ musico: String, a vieja: Seccion, en conciertos: [Concierto]) -> [Concierto] {
let nueva = Seccion(nombre: vieja.nombre, musicos: vieja.musicos + [musico])
return conciertos.map { $0.seccion === vieja ? Concierto(titulo: $0.titulo, seccion: nueva) : $0 }
}
let cuerdas = Seccion(nombre: "Cuerdas", musicos: ["Ana", "Luis"])
let vientos = Seccion(nombre: "Vientos", musicos: ["Marta"])
let conciertos = [
Concierto(titulo: "Apertura", seccion: cuerdas),
Concierto(titulo: "Gala", seccion: vientos),
Concierto(titulo: "Cierre", seccion: cuerdas),
]
let nuevos = incorporar("Bea", a: cuerdas, en: conciertos)
for concierto in nuevos {
let musicos = concierto.seccion.musicos.joined(separator: ", ")
print("\(concierto.titulo): \(concierto.seccion.nombre) (\(musicos))")
}
print("Apertura y Cierre comparten sección:", nuevos[0].seccion === nuevos[2].seccion)
print("Gala conserva su sección de antes:", nuevos[1].seccion === vientos)
print("La sección de antes no cambió:", cuerdas.musicos.count)La sección nueva se construye una sola vez, fuera del map, y se reparte a los dos conciertos que tenían la vieja: por eso «Apertura» y «Cierre» comparten sección. Si la construyeras dentro del map, saldría una sección distinta por concierto y la primera comprobación daría false. Con Concierto como struct, cada concierto es un valor nuevo; la Seccion es una clase, así que los dos apuntan al mismo objeto.
Paso 1 · El repositorio aprende a añadir jugadores
Objetivo: que RepositorioDePartidos tenga anadirJugador, y que el falso de las pruebas lo cumpla también.
Piezas nuevas de este paso:
private(set) var equipos: hasta hoy era unlet. Unletno cambia, así que@Observableno tiene nada que vigilar; convar, cuando la lista cambia, las pantallas que la leen se redibujan solas.private(set)deja que solo el repositorio la cambie.equipos.first(where: { … }): el primer equipo que cumple la condición, onilsi no hay ninguno.first { … }(sinwhere:, con la closure fuera del paréntesis) es la misma función escrita de otra forma; la verás más abajo.equipos.firstIndex(where: { … }): lo mismo, pero devuelve la posición del primero que cumple (onil), para poder escribirequipos[posición] = ….- La coma en un
guard(y en unif) es un «y»:guard let a = …, let b = … else { … }sigue solo si las dos condiciones se cumplen; si falla cualquiera, entra en elelse. Más de una condición, separadas por comas. @discardableResult: quien llama puede ignorar elBoolque devuelve la función, sin aviso del compilador (lo viste en struct frente a class).private extension Partido { … }: una extensión añade funciones a un tipo que ya existe, aunque no sea tuyo (Protocolos y extensiones). Conprivatedelante, lo que añade solo se ve dentro de este fichero.- Una función dentro de otra función:
cambiarsolo existe dentro decambiando, y la usa tres veces. eventos.map { evento -> EventoDePartido in … }: una closure con el tipo de lo que devuelve escrito antes dein. Hace falta cuando la closure tiene varias líneas y Swift no puede deducirlo solo. Ylet eventos = eventos.map …crea una constante local con el mismo nombre que la propiedad. Es legal: la parte derecha del=se evalúa primero y lee la propiedad; a partir de esa línea, dentro de la función,eventoses la constante nueva y la propiedad queda tapada. Mini ejemplo:let precio = 10y, dentro de una función,let precio = precio * 2da 20 en esa función y deja el 10 de fuera como estaba.if case let .gol(minuto, jugador, equipo) = evento: «si este evento es un gol, sácame sus datos» (Enums: el partido de eventos). Con los demás eventos, la condición falla y se devuelve el evento tal cual.
Primero el contrato. El protocolo gana una función. Intenta escribirla tú: se llama anadirJugador, recibe el jugador (sin etiqueta) y el nombre del equipo (con la etiqueta a), y devuelve un Bool: si se pudo añadir.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
protocol PartidosRepositorio: AnyObject {
var equipos: [Equipo] { get }
var partidos: [PartidoDeLista] { get }
func partido(id: Int) -> Partido?
func registrarGol(partidoId: Int, gol: EventoDePartido)
func anadirJugador(_ jugador: Jugador, a nombreDelEquipo: String) -> Bool
}El resto del fichero no cambia: el protocolo ya existía y solo se añade su última línea.
Después, en la clase, la propiedad equipos deja de ser un let y pasa a private(set) var:
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
private(set) var equipos: [Equipo]
private(set) var partidos: [PartidoDeLista]El resto del fichero no cambia.
Y al final de la clase, la función. Qué hace, en orden: busca el equipo viejo por nombre; construye el nuevo con Equipo(nombre:plantilla:), que vuelve a comprobar la regla del dominio (si el dorsal ya lo lleva otro, devuelve nil) y en ese caso la función devuelve false sin cambiar nada; sustituye el equipo en la lista y en cada partido; guarda el torneo y devuelve true. Debajo, la extensión privada que sustituye el equipo dentro de un partido: el local, el visitante y los goles.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Añadir un jugador. `Equipo` no se puede cambiar (sus propiedades son `let`): se construye
// OTRO con un jugador más y se pone en el sitio del viejo, en la lista de equipos y en cada
// partido donde juega. Su `init?` vuelve a comprobar la regla del dominio: si el dorsal se
// repitiera, devuelve `nil` y no se cambia nada. No hace falta ninguna migración: el JSON ya
// guardaba la plantilla de cada equipo (`EquipoGuardado.plantilla`).
@discardableResult
func anadirJugador(_ jugador: Jugador, a nombreDelEquipo: String) -> Bool {
guard let viejo = equipos.first(where: { $0.nombre == nombreDelEquipo }),
let nuevo = Equipo(nombre: viejo.nombre, plantilla: viejo.plantilla + [jugador])
else { return false }
equipos = equipos.map { $0 === viejo ? nuevo : $0 }
partidos = partidos.map { PartidoDeLista(id: $0.id, partido: $0.partido.cambiando(viejo, por: nuevo)) }
guardar()
return true
}
}
private extension Partido {
// El mismo partido con `nuevo` donde estaba `viejo`: de local, de visitante y en sus goles.
// `===` compara la IDENTIDAD: el mismo objeto, no uno con el mismo nombre.
func cambiando(_ viejo: Equipo, por nuevo: Equipo) -> Partido {
func cambiar(_ equipo: Equipo) -> Equipo { equipo === viejo ? nuevo : equipo }
let eventos = eventos.map { evento -> EventoDePartido in
if case let .gol(minuto, jugador, equipo) = evento {
return .gol(minuto: minuto, jugador: jugador, equipo: cambiar(equipo))
}
return evento
}
return Partido(local: cambiar(local), visitante: cambiar(visitante), eventos: eventos)
}
}La función anadirJugador va dentro de la clase, después de registrarGol; la llave } que la cierra es la de la clase, y la extensión va fuera, debajo. El resto del fichero no cambia.
Las tarjetas y los cambios no llevan equipo, así que cambiando los deja como están: solo los goles se reescriben.
Ahora el falso de las pruebas, que también tiene que cumplir el contrato. En Dobles.swift, dentro de RepositorioFalso, justo debajo de registrarGol. El falso hace lo mismo que el real, sin disco ni partidos: sustituye el equipo en su lista (que ya es una var, la de equipos) y anota el jugador para que una prueba pueda preguntarlo. Para anotarlo usa una lista de tuplas con nombre, [(jugador: Jugador, equipo: String)]: cada elemento junta un jugador y un equipo (viste las tuplas en Colecciones y key paths), y append((jugador, nombreDelEquipo)) lleva dobles paréntesis porque el interior es la tupla que se añade.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Anota el jugador y lo suma a la plantilla (un equipo nuevo, como el de verdad).
private(set) var jugadoresAnadidos: [(jugador: Jugador, equipo: String)] = []
func anadirJugador(_ jugador: Jugador, a nombreDelEquipo: String) -> Bool {
guard let indice = equipos.firstIndex(where: { $0.nombre == nombreDelEquipo }),
let nuevo = Equipo(nombre: nombreDelEquipo, plantilla: equipos[indice].plantilla + [jugador])
else { return false }
equipos[indice] = nuevo
jugadoresAnadidos.append((jugador, nombreDelEquipo))
return true
}El resto del fichero no cambia.
Compila con Cmd+B: debe terminar con Build Succeeded.
Paso 2 · Las pantallas de equipos y de plantilla
Objetivo: el botón «Equipos», la lista de equipos y la plantilla de cada uno.
Piezas nuevas de este paso:
LabeledContent("Rayo FC", value: "3 jugadores"): una fila con un texto a la izquierda y un valor a la derecha, que es lo que hacías a mano con unHStacky unSpacer. Además, el lector de pantalla (VoiceOver, que lee en voz alta lo que hay en la pantalla) lo lee como un solo elemento, «Rayo FC, 3 jugadores», en vez de dos textos sueltos. Como mínimo:List { LabeledContent("Jugadores", value: "11") }pinta «Jugadores» a la izquierda y «11» a la derecha, en gris.List(lista, id: \.nombre): dice qué propiedad identifica a cada elemento.\.nombrees un key path, «el camino hasta la propiedadnombre» (Colecciones y key paths); como enid: \.id, de La identidad de una vista.equipo.cantidadDeJugadores: ya existe enEquipo, desde struct frente a class; aquí solo se lee.- Dentro del cuerpo de una vista (y de la closure de una fila) puedes declarar una constante con
letantes de devolver la vista, comolet cantidad = …más abajo. .toolbar { … }: lo que pones dentro va a la barra de arriba. CadaNavigationLinkes un botón de texto; en una barra de navegación van a la derecha, en el orden en que los escribes.condición ? a : b, el operador ternario (Opcionales): aquí elige entre «1 jugador» y «3 jugadores», porque el plural depende del número.
Cuatro cambios (dos ficheros nuevos y dos modificados) y un proyecto en rojo hasta el último: Destino gana dos casos, y el switch de ListaDePartidosView (Xcode: switch must be exhaustive) no compila hasta que los atiendes. Empieza por Destino.swift: un caso equipos y otro plantilla, que lleva el nombre del equipo.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
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)
case cronica(Int) // la crónica del partido con ese id (botón «Crónica» del partido)
case equipos // la lista de equipos (botón «Equipos» de la barra de Partidos)
case plantilla(String) // la plantilla del equipo con ese nombre
}La plantilla es una funcionalidad nueva, y cada funcionalidad tiene su carpeta. Se llama Plantilla y no Equipos porque la funcionalidad es la plantilla de jugadores de cada equipo; la lista de equipos (EquiposView) es solo su puerta de entrada. Clic derecho sobre la carpeta MarcadorApp con el código → New Folder, de nombre Plantilla; clic derecho sobre Plantilla → New Folder, de nombre UI (con la Terminal: mkdir -p ~/Marcador-Swift/MarcadorApp/MarcadorApp/Plantilla/UI). Sus datos no necesitan carpeta propia: son los de Partidos, que ya existe.
Crea EquiposView.swift: clic derecho sobre Plantilla/UI → New File from Template… → Swift File, solo con el target MarcadorApp. Intenta escribirlo tú: una vista que lee el repositorio de partidos del entorno y pinta una List con un NavigationLink(value:) por equipo, hacia Destino.plantilla con su nombre. Cada fila es un LabeledContent con el nombre y «N jugadores» (o «1 jugador»). Título «Equipos», pequeño en la barra. Con vista previa dentro de una NavigationStack y un repositorio nuevo en el entorno.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import SwiftUI
import Torneo
// La lista de equipos: cada fila empuja la plantilla de ese equipo. Lee el repositorio
// (`@Observable`): si mañana se añade un equipo, aparece solo.
struct EquiposView: View {
@Environment(RepositorioDePartidos.self) private var repositorio
var body: some View {
List(repositorio.equipos, id: \.nombre) { equipo in
NavigationLink(value: Destino.plantilla(equipo.nombre)) {
// «1 jugador», pero «3 jugadores»: el plural depende del número.
let cantidad = equipo.cantidadDeJugadores
LabeledContent(equipo.nombre, value: cantidad == 1 ? "1 jugador" : "\(cantidad) jugadores")
}
}
.navigationTitle("Equipos")
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
NavigationStack { EquiposView() }
.environment(RepositorioDePartidos())
}Y PlantillaView.swift, creado igual y en la misma carpeta, Plantilla/UI. Intenta escribirlo tú: recibe el nombre del equipo, lee el repositorio del entorno y busca ese equipo en repositorio.equipos en cada pasada del body; así, cuando se añada un jugador, el repositorio cambia y la lista se redibuja sola. Una List con un LabeledContent por jugador: su nombre y dorsalOGuion(). El título es el nombre del equipo, pequeño en la barra.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import SwiftUI
import Torneo
// La plantilla de un equipo: una fila por jugador, con su dorsal o un guion. El equipo llega por
// su NOMBRE (el del `Destino`) y se busca en el repositorio en cada pasada del body: cuando se
// añada un jugador, el repositorio cambia y esta lista se redibuja sola.
struct PlantillaView: View {
let equipo: String
@Environment(RepositorioDePartidos.self) private var repositorio
private var jugadores: [Jugador] {
repositorio.equipos.first { $0.nombre == equipo }?.plantilla ?? []
}
var body: some View {
List(jugadores, id: \.nombre) { jugador in
LabeledContent(jugador.nombre, value: jugador.dorsalOGuion())
}
.navigationTitle(equipo)
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
NavigationStack { PlantillaView(equipo: "Rayo FC") }
.environment(RepositorioDePartidos())
}¿Por qué se pasa el nombre y no el Equipo? Porque el equipo viejo deja de existir cuando se añade un jugador: una pantalla que guardara el objeto seguiría enseñando la plantilla de antes. Con el nombre, busca el vigente cada vez.
Por último, en ListaDePartidosView.swift, el botón y los dos casos. En el .toolbar, «Equipos» va delante de «Árbitros» (sustituye también el comentario de encima por el de abajo, porque ahora hay dos botones):
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Dos botones en la barra de arriba: empujan la lista de equipos y la de árbitros.
.toolbar {
NavigationLink("Equipos", value: Destino.equipos)
NavigationLink("Árbitros", value: Destino.arbitros)
}Y en el switch de .navigationDestination, después del caso .cronica:
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
case .equipos:
EquiposView()
case let .plantilla(equipo):
PlantillaView(equipo: equipo)El resto del fichero no cambia.
Ejecuta con Cmd+R. En «Partidos» ves dos botones arriba a la derecha, «Equipos» y «Árbitros». Toca «Equipos»: cuatro filas, «Rayo FC», «Toros» y «Lobos» con sus jugadores, y «Águilas» con «1 jugador». Toca «Rayo FC»: «Ana #9», «Luis -» y «Marta #1». El botón de atrás vuelve a la lista.
Paso 3 · La prueba de que un jugador añadido juega
Objetivo: una prueba que demuestre las tres cosas que importan: el jugador entra en los partidos de su equipo, puede marcar, y sobrevive a cerrar la app.
Pieza nueva: try #require(…) es como #expect, pero para un valor que puede ser nil: si lo es, la prueba se detiene ahí y falla; si no, te da el valor ya sin el ?. Las demás piezas (la carpeta temporal, AlmacenDelTorneo, volver a abrir un repositorio con el mismo almacén) las escribiste en Guardar el torneo en un fichero.
Añade esta prueba a MarcadorAppTests.swift, debajo de elTorneoSobreviveAGuardarseYCargarse: aunque pruebe a RepositorioDePartidos (que vive en Partidos/Data), va en este fichero de la raíz de las pruebas, el de la plantilla de Xcode, porque ahí está ya la prueba de guardar el torneo y esta es su continuación. Qué hace, en orden: añade a Bea con el 7 al Rayo FC; comprueba que el partido 1, cuyo local es el Rayo, la lleva ya en su plantilla; registra un gol suyo; comprueba que un segundo jugador con el 7 se rechaza; y abre otro repositorio con el mismo almacén para comprobar que Bea sigue en la plantilla y que su gol sigue en el partido.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Un jugador añadido entra en su equipo y en sus partidos, puede marcar y sobrevive a cerrar la app.
@Test func unJugadorAnadidoJuegaYSeGuarda() throws {
let carpeta = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: carpeta, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: carpeta) }
let almacen = AlmacenDelTorneo(url: carpeta.appending(path: "torneo.json"))
let bea = Jugador(nombre: "Bea", dorsal: 7)
let repositorio = RepositorioDePartidos(almacen: almacen)
#expect(repositorio.anadirJugador(bea, a: "Rayo FC"))
// El partido 1 es Rayo FC - Toros: su local ya la lleva en la plantilla, y puede marcar.
let partido = try #require(repositorio.partido(id: 1))
#expect(partido.local.plantilla.contains(bea))
repositorio.registrarGol(partidoId: 1, gol: .gol(minuto: 3, jugador: bea, equipo: partido.local))
// Un dorsal repetido lo rechaza el dominio (`Equipo.init?`).
#expect(!repositorio.anadirJugador(Jugador(nombre: "Otra", dorsal: 7), a: "Rayo FC"))
let recargado = RepositorioDePartidos(almacen: almacen)
#expect(recargado.equipos.first { $0.nombre == "Rayo FC" }?.plantilla.last == bea)
#expect(recargado.partido(id: 1)?.goleadores().last == bea)
}El resto del fichero no cambia: es una prueba más dentro de la misma struct.
Lanza Cmd+U.
Punto de control
- Cmd+B termina en
Build Succeededy Cmd+U da 51 pruebas en verde: 35 unitarias y 16 de interfaz (con tus ejercicios de pruebas, 54). La nueva esunJugadorAnadidoJuegaYSeGuarda. - En el simulador, «Partidos» tiene dos botones, «Equipos» y «Árbitros». «Equipos» enseña «Rayo FC» con 3 jugadores, «Toros» con 2, «Lobos» con 2 y «Águilas» con 1 jugador. Tocar «Rayo FC» enseña «Ana #9», «Luis -» y «Marta #1», y el botón de atrás vuelve.
- Las 16 pruebas de interfaz de antes siguen en verde: la barra con dos enlaces no rompe ninguna.
- El ejercicio de la orquesta imprime las seis líneas de arriba.
- Sabes decir por qué
anadirJugadorsustituye el equipo en los partidos y no solo en la lista, y por qué en iOS no hizo falta una migración.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.
Equipoguarda su plantilla en unlet. ¿Cómo añadeanadirJugadorun jugador?2.Se sustituye el equipo local de un partido, pero no sus goles. ¿Qué pasa?
3.En
equipos.map { $0 === viejo ? nuevo : $0 }, ¿qué hace===?4.¿Por qué en iOS añadir jugadores no necesita ninguna migración?
5.El Rayo FC ya tiene un jugador con el 7. ¿Qué devuelve
anadirJugadorcon otro jugador con el 7?