Saltar al contenido
K
Swift: el lenguajePaso a paso35 min de lectura · 10 min de máquina Intermedio

Enums: el partido de eventos

Al terminar: Resolver el partido a partir de una lista de eventos con enums.

Al terminar esta lección Partido guarda en Swift lo que pasó —goles, tarjetas, cambios— y el marcador sale de ahí, igual que en tu versión de Kotlin. Hasta ahora un Partido guardaba su marcador en dos números; desde ahora guarda sus eventos y calcula el marcador, así que no puede contradecirse. xcrun swift run torneo-cli imprimirá la crónica de un partido y su resultado.

Temas: if case, count { … }, Equatable en un enum, métodos que cambian el valor frente a métodos que devuelven una copia, y deducir un dato en lugar de guardarlo.

Guardar el marcador o deducirlo

Si Partido guarda golesLocal = 2 y a la vez una lista de eventos con tres goles locales, ¿cuál dice la verdad? Con dos fuentes de datos, pueden contradecirse. La solución es guardar una sola: la lista de lo que pasó. El marcador deja de ser un dato y pasa a ser una cuenta: «cuántos de estos eventos son goles del Rayo». Por eso en el paso se borra todo lo que guardaba el marcador: golesLocal y golesVisitante como datos, y conGolLocal(), conGolVisitante(), registrarGolLocal() y registrarGolVisitante().

Para escribir esa cuenta hacen falta unas piezas que todavía no has visto.

Las piezas que vas a escribir

  • if case let .gol(_, _, equipoDelGol) = evento { … } else { … }: un switch de un solo caso. Entra en el if solo si el evento es un gol y liga su equipo. Como el if es una expresión, cada rama es el valor: equipoDelGol == equipo si es un gol y false si no. Si solo quieres saber qué caso es y no necesitas sus datos, no ligas nada: if case .gol = evento (sin let ni paréntesis).
  • eventos.count { evento in … }: cuenta los elementos para los que el bloque devuelve true. Lo que va entre llaves, evento in …, es una closure: una pequeña función sin nombre, donde evento es su parámetro (cada elemento de la lista) y lo que sigue a in es lo que hace; se explica a fondo en Closures. Aquí devuelve true o false. Se lee en dos pasos: «por cada evento, ¿es un gol de este equipo?» y «cuántos dijeron que sí».
  • Equatable en EventoDePartido: ya lo declaraste en la lección anterior. Es lo que permite comparar equipoDelGol == equipo y que Partido: Equatable siga compilando.
  • mutating func registrar(_:) y func registrando(_:): convención de nombres de Swift. Un verbo (registrar) cambia el propio valor y un gerundio (registrando) devuelve una copia. Kotlin solo tenía la segunda, con el nombre registrar.
  • var copia = self: dentro de un método de un struct, self se copia como cualquier valor.
  • Encadenar en varias líneas: un punto al principio de la línea siguiente sigue actuando sobre lo anterior.
  • Un if/else if como expresión en resultado(): cada rama es el valor, sin return.
  • .map(\.rawValue): ya lo viste en la lección anterior; aquí saca el texto de cada color de tarjeta.

Las mismas piezas, en otro dominio: los movimientos de una cuenta del banco. No es tu código de Marcador; pruébalo en tu Playground de Xcode (el de Tu primer paquete de Swift; reutilízalo: borra lo anterior, pega el código y ejecútalo).

ejemplo mínimo: las piezas del paso, con una cuenta del banco
enum Movimiento: Equatable {                // Equatable: lo que guardan los casos (Int, String) ya lo es
    case ingreso(dia: Int, importe: Int)
    case retiro(dia: Int, importe: Int)
    case comision(dia: Int, concepto: String)
 
    var dia: Int {                          // el dato que comparten los tres casos
        switch self {
        // Tres patrones en una rama, separados por comas; cada uno liga dia y _ ignora el resto.
        case .ingreso(let dia, _), .retiro(let dia, _), .comision(let dia, _):
            dia
        }
    }
}
 
enum Estado: Equatable { case enPositivo, enRojo }   // dos casos simples en una línea, separados por coma
 
struct Cuenta: Equatable {
    private(set) var movimientos: [Movimiento] = []
 
    func ingresosDe(masDe minimo: Int) -> Int {
        // count { … } cuenta los elementos para los que el bloque devuelve true.
        movimientos.count { movimiento in
            // if case: entra solo si es un ingreso, y liga su importe.
            if case let .ingreso(_, importe) = movimiento { importe > minimo } else { false }
        }
    }
 
    mutating func anotar(_ movimiento: Movimiento) {     // verbo: cambia self
        movimientos.append(movimiento)
    }
 
    func anotando(_ movimiento: Movimiento) -> Cuenta {  // gerundio: devuelve una copia
        var copia = self                    // self es un struct: esto es una copia
        copia.anotar(movimiento)            // se cambia la copia, no self
        return copia
    }
 
    func estado(saldo: Int) -> Estado {
        if saldo >= 0 { .enPositivo } else { .enRojo }  // el if es el valor; .enRojo es un Estado
    }
}
 
let vacia = Cuenta()
let llena = vacia                           // cada línea que empieza por punto sigue la cadena
    .anotando(.ingreso(dia: 1, importe: 500))
    .anotando(.retiro(dia: 3, importe: 80))
    .anotando(.ingreso(dia: 9, importe: 40))
print(vacia.movimientos.count, llena.movimientos.count)   // 0 3: vacia no cambió
print(llena.movimientos.map(\.dia))         // [1, 3, 9]: map con un key path
print(llena.ingresosDe(masDe: 100))         // 1: solo el de 500
print(llena.estado(saldo: 460) == .enPositivo)   // true

Imprime 0 3, [1, 3, 9], 1 y true.

Reto · Ejercicio: la cesta de la compra

En tu Playground (borra antes lo anterior), un artículo de la cesta puede ser una fruta (con su nombre y los kilos) o una bebida (con su nombre y los litros). Haz una cesta que guarde sus artículos (se leen desde fuera, solo cambian dentro) y que tenga: una función que mete un artículo en la propia cesta; otra que devuelve una copia con el artículo metido, sin tocar la original; los kilos de fruta que lleva; y cuántas bebidas lleva. Parte de una cesta vacía, saca de ella una copia con 2 kg de manzanas y 6 l de agua (encadenando), mete en esa copia 1 kg de peras e imprime: cuántos artículos tiene la vacía y cuántos la llena (en una línea), los kilos de fruta y el número de bebidas.

0 3
3
1
Ver solución
solución: la cesta de la compra
enum Articulo: Equatable {
    case fruta(nombre: String, kilos: Int)
    case bebida(nombre: String, litros: Int)
}
 
struct Cesta {
    private(set) var articulos: [Articulo] = []
 
    mutating func meter(_ articulo: Articulo) {
        articulos.append(articulo)
    }
 
    func metiendo(_ articulo: Articulo) -> Cesta {
        var copia = self
        copia.meter(articulo)
        return copia
    }
 
    func kilosDeFruta() -> Int {
        var total = 0
        for articulo in articulos {
            if case let .fruta(_, kilos) = articulo { total += kilos }
        }
        return total
    }
 
    func cuantasBebidas() -> Int {
        articulos.count { articulo in
            if case .bebida = articulo { true } else { false }
        }
    }
}
 
let vacia = Cesta()
var llena = vacia
    .metiendo(.fruta(nombre: "manzanas", kilos: 2))
    .metiendo(.bebida(nombre: "agua", litros: 6))
llena.meter(.fruta(nombre: "peras", kilos: 1))
print(vacia.articulos.count, llena.articulos.count)
print(llena.kilosDeFruta())
print(llena.cuantasBebidas())

Imprime 0 3, 3 y 1. meter lleva mutating porque cambia la propia cesta (por eso llena es var); metiendo trabaja sobre una copia y la original, vacia, se queda con 0 artículos.

Paso · El partido hecho de eventos

Objetivo. Reescribir Partido para que guarde sus eventos y deduzca el marcador, y ver la crónica de un partido en main.swift. Al terminar, xcrun swift run torneo-cli imprime la crónica y su resultado.

Torneo
Sources
Torneo
Partido.swiftse reescribe
torneo-cli
main.swiftse modifica
Tests
TorneoTests
EnumsTests.swiftse crea y se rellena con «Copiar fichero»
StructYClassTests.swiftse sustituye con «Copiar fichero»
Lo que tocas, en el Project navigator de Xcode con el paquete Torneo abierto. Dos ficheros que cambian y dos de tests.

El título de cada bloque es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.

Lo que se borra: el marcador guardado

Partido guardaba golesLocal y golesVisitante y tenía conGolLocal(), conGolVisitante(), registrarGolLocal() y registrarGolVisitante(). Todo eso se borra: el marcador sale de los eventos. Para reescribir Partido.swift entero, ábrelo (doble clic en el Project navigator), selecciona todo con Cmd+A y escribe encima. En main.swift (Sources → torneo-cli) se sustituye todo el trozo que va desde la línea var alFinal = inicio hasta la línea print("¿Mismo equipo local en las dos copias? …"), ambas incluidas (son las tres llamadas a registrarGol…, el print de «Antes», el comentario y el último print); el resto del fichero no cambia.

Lo que tiene que ofrecer Partido. Los tests que copiarás usan estos nombres y textos exactos:

TipoQué lleva
struct Partido: Equatablelocal, visitante, eventos (se lee fuera, se cambia dentro); init(local:visitante:eventos:) con eventos vacío por defecto; golesDe(_ equipo:) -> Int; golesLocal y golesVisitante calculados; mutating registrar(_:); registrando(_:) -> Partido; resultado() -> Resultado

Intenta escribirlo tú, en este orden:

  1. El nuevo Partido: guarda sus eventos (empieza sin ninguno) y cuenta los goles de cada equipo a partir de ellos; registrar añade un evento al propio partido, registrando devuelve una copia con el evento añadido, y resultado compara los goles.
  2. La crónica en main.swift (sustituye el trozo indicado arriba): a partir de inicio, registra en cadena un gol de Ana en el 12, una amarilla a Iván en el 30, un gol de Iván para Toros en el 55, otro de Ana en el 80 y, en el 85, el cambio de Ana por el segundo jugador de la lista; imprime cuántos eventos hay antes y después, cada evento descrito, el marcador con el titular y los textos de los colores de tarjeta.

Lo que debes ver, justo antes de la última línea de la salida (Partidos vistos desde …):

Antes: 0 eventos, después: 5
12' Gol de Ana (Rayo FC)
30' Tarjeta amarilla para Iván
55' Gol de Iván (Toros)
80' Gol de Ana (Rayo FC)
85' Cambio: sale Ana, entra Luis
Marcador: 2-1 -> Gana Rayo FC
Colores de tarjeta: ["AMARILLA", "ROJA"]

Cuando termines, abre el bloque y compara con la solución.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/Partido.swift
public struct Partido: Equatable {
    public let local: Equipo
    public let visitante: Equipo
    public private(set) var eventos: [EventoDePartido]
 
    public init(local: Equipo, visitante: Equipo, eventos: [EventoDePartido] = []) {
        self.local = local
        self.visitante = visitante
        self.eventos = eventos
    }
 
    public func golesDe(_ equipo: Equipo) -> Int {
        eventos.count { evento in
            // `if case` es un `switch` de un solo caso: mira si el evento es un
            // gol y, si lo es, liga el equipo.
            if case let .gol(_, _, equipoDelGol) = evento { equipoDelGol == equipo } else { false }
        }
    }
 
    public var golesLocal: Int { golesDe(local) }
    public var golesVisitante: Int { golesDe(visitante) }
 
    // Convención de nombres de Swift: el que CAMBIA `self` es un verbo
    // (`registrar`) y el que devuelve una copia usa un gerundio
    // (`registrando`). Kotlin solo tenía el segundo, con el nombre `registrar`.
    public mutating func registrar(_ evento: EventoDePartido) {
        eventos.append(evento)
    }
 
    public func registrando(_ evento: EventoDePartido) -> Partido {
        var copia = self
        copia.registrar(evento)
        return copia
    }
 
    public func resultado() -> Resultado {
        if golesLocal > golesVisitante {
            .victoria(ganador: local, perdedor: visitante)
        } else if golesLocal < golesVisitante {
            .victoria(ganador: visitante, perdedor: local)
        } else {
            .empate
        }
    }
}
Torneo/Sources/torneo-cli/main.swift
// Parte del partido de antes (`inicio`, sin eventos) y registra cinco eventos.
// El punto inicial (`.gol(…)`) funciona porque Swift sabe que `registrando` espera un `EventoDePartido`.
let alFinal = inicio
    .registrando(.gol(minuto: 12, jugador: ana, equipo: rayo))        // gol de Ana en el 12'
    .registrando(.tarjeta(minuto: 30, jugador: ivan, color: .amarilla)) // amarilla a Iván
    .registrando(.gol(minuto: 55, jugador: ivan, equipo: toros))      // gol de Iván para Toros
    .registrando(.gol(minuto: 80, jugador: ana, equipo: rayo))        // otro de Ana
    .registrando(.cambio(minuto: 85, sale: ana, entra: jugadores[1])) // Ana sale, entra Luis
// `inicio` no cambia: `registrando` devuelve una copia con el evento añadido.
print("Antes: \(inicio.eventos.count) eventos, después: \(alFinal.eventos.count)")
// Una línea por evento, con el texto que construye `describir`.
for evento in alFinal.eventos { print(describir(evento)) }
// Los goles salen de los eventos; `resultado()` los compara y `titular` da el texto.
print("Marcador: \(alFinal.golesLocal)-\(alFinal.golesVisitante) -> \(alFinal.resultado().titular)")
// `allCases` trae los dos colores y `rawValue` el texto de cada uno.
print("Colores de tarjeta: \(ColorDeTarjeta.allCases.map(\.rawValue))")

Los tests no los escribes: son comprobaciones de los enums y del Partido que acabas de crear, del mismo tipo que las de las lecciones anteriores. Hay dos ficheros. EnumsTests.swift es nuevo, y StructYClassTests.swift ya existe y se sustituye por una versión adaptada a los eventos (sus 12 tests siguen siendo los mismos). Se rellenan con el botón Copiar fichero, que copia el contenido al portapapeles y no escribe nada en tu proyecto (se explica en Clases y objetos). El orden es siempre el mismo: crear o abrir el fichero, pulsar el botón, seleccionar todo, pegar.

  1. En el Project navigator, clic derecho sobre la carpeta Tests → TorneoTests → New File from Template… → Swift File → Next → EnumsTests → Create. (Plan B: touch Tests/TorneoTests/EnumsTests.swift desde ~/Marcador-Swift/Torneo.) Se abre con unas pocas líneas de plantilla.
  2. Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
  3. Haz clic dentro de EnumsTests.swift, selecciona todo con Cmd+A y pega con Cmd+V: sustituye lo que puso Xcode.
EnumsTests.swift

Ahora el que ya existe: abre StructYClassTests.swift, pulsa su botón, selecciona todo con Cmd+A y pega con Cmd+V. Pisa tu versión anterior, y es lo que quieres.

StructYClassTests.swift

Punto de control

  • Build. Desde ~/Marcador-Swift/Torneo, xcrun swift build termina en Build complete!.
  • Run. xcrun swift run torneo-cli termina con estas líneas:
Antes: 0 eventos, después: 5
12' Gol de Ana (Rayo FC)
30' Tarjeta amarilla para Iván
55' Gol de Iván (Toros)
80' Gol de Ana (Rayo FC)
85' Cambio: sale Ana, entra Luis
Marcador: 2-1 -> Gana Rayo FC
Colores de tarjeta: ["AMARILLA", "ROJA"]
Partidos vistos desde `torneo`: 1
  • Test. xcrun swift test termina con Test run with 34 tests in 5 suites passed: los 28 anteriores y los 6 de EnumsTests.swift (StructYClassTests.swift se sustituye por una versión con los mismos 12).
  • Provocado. En resultado() cambia el primer > por <: xcrun swift test falla (en EnumsTests, el test del resultado de una victoria y un empate). Devuélvelo a como estaba.
  • Sabes decir por qué Partido ya no guarda los goles y qué diferencia hay entre registrar y registrando.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.En golesDe, la línea if case let .gol(_, _, equipoDelGol) = evento { … } else { false } mira un evento. ¿Qué hace?

  2. 2.¿Qué cuenta eventos.count { evento in … }?

  3. 3.Tienes let alFinal = inicio.registrando(.gol(…)). ¿Qué pasa con inicio?

  4. 4.¿Por qué Partido ya no guarda golesLocal y golesVisitante como datos?

Faltan 4 de 4