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

Closures con listas

Al terminar: Usar closures con listas: map, filter y ordenar.

Al terminar esta lección podrás preguntarle a un partido en Swift cosas como «dame solo las tarjetas», «dame los goleadores» o «dame los minutos de gol de Ana», y cada pregunta será una línea, igual que en tu versión de Kotlin. Es lo que se consigue al juntar las closures de la lección anterior con las listas: filter para quedarte con unos elementos, compactMap para transformar y descartar, forEach para recorrer y map para transformar.

Temas: filter, compactMap, forEach con guard case, map con el nombre de una función o un key path, y una propiedad y una función con el mismo nombre.

filter, compactMap, forEach y map: preguntas a una lista

Una lista (un array) guarda varios valores en orden. Cuatro de sus funciones reciben una closure y la aplican a cada elemento:

  • filter { … } se queda con los elementos para los que la closure da true y devuelve una lista nueva con ellos.
  • map { … } transforma cada elemento con la closure y devuelve la lista de los resultados, en el mismo orden. En lugar de la closure puedes pasar el nombre de una función (map(describir)) o un key path (map(\.nombre)): una forma corta de decir «de cada elemento, su nombre», equivale a { $0.nombre }; ya viste uno, \.rawValue, en Enums con valores asociados.
  • compactMap { … } transforma cada elemento, pero la closure puede devolver nil («nada») y esos resultados se descartan. Sirve para «quédate con esto de los elementos que lo tengan».
  • forEach { … } ejecuta la closure una vez por elemento y no devuelve nada.

Ninguna cambia la lista original: devuelven otra. Una nota sobre cómo se escriben las closures de abajo: if case .cafe = $0 { true } else { false } es un if usado como expresión, así que cada rama es el valor que devuelve la closure, sin return (lo mismo que hiciste en Enums: el partido de eventos). En Kotlin no había compactMap: se escribía filterIsInstance y después map. En Swift no existe is para saber qué caso es un enum; se usa if case, y compactMap recoge lo que sí encaja.

Dentro de un forEach, un return sale solo de la closure, es decir, de la vuelta actual, y forEach sigue con el elemento siguiente: es la forma de decir «salta este». Con guard case let .gol(…) = evento else { return } se saltan los eventos que no son goles; si es un gol, sus datos valen para el resto de la closure. Un detalle: Partido ya tiene una propiedad eventos y ahora también tendrá una función eventos(_:). Son cosas distintas con el mismo nombre; con paréntesis o una closure final (alFinal.eventos { … }) Swift elige la función, y sin ellos la propiedad.

Dónde se prueba. En tu Playground de Xcode, el de Tu primer paquete de Swift: reutilízalo, borra lo anterior, pega el código, ejecútalo y mira la consola.

ejemplo mínimo: filter, compactMap, forEach y map, con los pedidos de una cafetería
enum Pedido {                              // como EventoDePartido: casos con datos
    case cafe(mesa: Int)
    case tostada(mesa: Int, extra: String)
}
 
// Una constante cuyo valor es una closure: ¿es un café?
let esCafe: (Pedido) -> Bool = {
    if case .cafe = $0 { true } else { false }
}
 
// La función DEVUELVE una closure; `{ $0 > minimo }` recuerda `minimo`.
func masDe(_ minimo: Int) -> (Int) -> Bool {
    { $0 > minimo }
}
 
let pedidos: [Pedido] = [.cafe(mesa: 1), .tostada(mesa: 2, extra: "tomate"), .cafe(mesa: 2)]
 
let cafes = pedidos.filter(esCafe)         // filter se queda con los que dan true
let extras = pedidos.compactMap { pedido in
    if case let .tostada(_, extra) = pedido { extra } else { nil }   // nil se descarta
}
var mesasConTostada: [Int] = []
pedidos.forEach { pedido in                // una vuelta por pedido
    guard case let .tostada(mesa, _) = pedido else { return }  // no es tostada: sale de esta vuelta
    mesasConTostada.append(mesa)           // aquí `mesa` ya vale
}
 
func nombre(_ pedido: Pedido) -> String {  // una función normal, para pasársela a map
    switch pedido {
    case .cafe: "café"
    case .tostada: "tostada"
    }
}
 
print(cafes.count, extras, mesasConTostada)          // 2 ["tomate"] [2]
print(pedidos.map(nombre))                           // map con el nombre de una función
print([5, 20, 8].filter(masDe(6)))                   // [20, 8]: filter con una closure devuelta por una función

Imprime 2 ["tomate"] [2], ["café", "tostada", "café"] y [20, 8].

Tu Kotlin y este Swift, línea a línea

Kotlin (tus Partido.kt y Main.kt)Swift (Partido.swift)Diferencia
eventos.filter(filtro)eventos.filter(filtro)Igual; también acepta el nombre de una función
filterIsInstance<Gol>().map(Gol::jugador)compactMap { if case let .gol(_, jugador, _) = evento { jugador } else { nil } }No hay is para los casos de un enum: if case liga el dato y compactMap descarta los nil
map(::describir)map(describir)Sin ::
return@forEachreturnEn Swift, return dentro de una closure sale solo de la closure

Reto · Ejercicio: los préstamos de la biblioteca

En tu Playground (borra antes lo anterior), un préstamo puede ser un libro (con su título y los días que lleva prestado) o una revista (con su título). Escribe una función que dé el título de cualquier préstamo. Con tres préstamos —el libro «Dune» con 20 días, la revista «Hola» y el libro «Momo» con 5 días—, imprime: los títulos de los préstamos que son libros, los días de cada libro y los títulos de los libros que llevan más de 14 días (recorriendo los préstamos uno a uno y saltando los que no son libros).

["Dune", "Momo"]
[20, 5]
["Dune"]
Ver solución
solución: los préstamos de la biblioteca
enum Prestamo {
    case libro(titulo: String, dias: Int)
    case revista(titulo: String)
}
 
func titulo(_ prestamo: Prestamo) -> String {
    switch prestamo {
    case let .libro(titulo, _): titulo
    case let .revista(titulo): titulo
    }
}
 
let prestamos: [Prestamo] = [
    .libro(titulo: "Dune", dias: 20),
    .revista(titulo: "Hola"),
    .libro(titulo: "Momo", dias: 5),
]
 
let soloLibros = prestamos.filter { prestamo in
    if case .libro = prestamo { true } else { false }
}
let diasDeCadaLibro = prestamos.compactMap { prestamo in
    if case let .libro(_, dias) = prestamo { dias } else { nil }
}
var atrasados: [String] = []
prestamos.forEach { prestamo in
    guard case let .libro(titulo, dias) = prestamo else { return }
    if dias > 14 { atrasados.append(titulo) }
}
 
print(soloLibros.map(titulo))
print(diasDeCadaLibro)
print(atrasados)

Imprime ["Dune", "Momo"], [20, 5] y ["Dune"]. filter se queda con los libros, compactMap saca los días y descarta la revista (que da nil), y el return del guard salta la revista sin salir del forEach.

Paso · Preguntarle cosas al partido

Objetivo. Añadir a Partido tres funciones (eventos(_:), goleadores() y minutosDeGolDe(_:)) y ver el resultado en main.swift. Al terminar, xcrun swift run torneo-cli imprime cuatro líneas nuevas antes de las de Vuelta.

Torneo
Sources
Torneo
Partido.swiftse modifica
torneo-cli
main.swiftse modifica
Tests
TorneoTests
ClosuresTests.swiftse crea y se rellena con «Copiar fichero»
Lo que tocas, en el Project navigator de Xcode con el paquete Torneo abierto.

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 tiene que ofrecer Partido. Los tests que copiarás usan estos nombres:

NombreQué recibe y qué hace
eventos (una función, sin etiqueta)recibe un filtro y devuelve los eventos que lo cumplen
goleadoresel jugador de cada gol, en orden
minutosDeGolDerecibe un jugador (sin etiqueta) y devuelve los minutos de sus goles

Intenta escribirlo tú: las tres funciones van en Partido.swift, dentro del struct, tras resultado() y antes de la llave que lo cierra (el resto del fichero no cambia). Después, en main.swift, justo debajo del comentario // Closures: filtrar eventos y sacar goleadores. y encima de la línea repetir(2), escribe unas líneas que, con el partido alFinal, impriman sus tarjetas descritas, cuántos eventos hay antes del minuto 45, los nombres de los goleadores y los minutos de gol de Ana. Guarda cada resultado en una constante y luego imprímela. Lo que debes ver:

Tarjetas: ["30\' Tarjeta amarilla para Iván"]
Primera mitad: 2 eventos
Goleadores: ["Ana", "Iván", "Ana"]
Minutos de gol de Ana: [12, 80]

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 func eventos(_ filtro: (EventoDePartido) -> Bool) -> [EventoDePartido] {
        eventos.filter(filtro)
    }
 
    public func goleadores() -> [Jugador] {
        eventos.compactMap { evento in
            if case let .gol(_, jugador, _) = evento { jugador } else { nil }
        }
    }
 
    public func minutosDeGolDe(_ jugador: Jugador) -> [Int] {
        var minutos: [Int] = []
        eventos.forEach { evento in
            guard case let .gol(minuto, autor, _) = evento else { return }
            if autor == jugador { minutos.append(minuto) }
        }
        return minutos
    }

Las funciones de Partido, comentadas línea a línea:

Partido: las tres funciones, comentadas
    // `filtro` es una closure: recibe un evento y dice si entra (`true`) o no (`false`).
    public func eventos(_ filtro: (EventoDePartido) -> Bool) -> [EventoDePartido] {
        eventos.filter(filtro)   // `filter` se queda con los eventos para los que `filtro` da `true`
    }
 
    public func goleadores() -> [Jugador] {
        // `compactMap` aplica la closure a cada evento y descarta los `nil`.
        eventos.compactMap { evento in
            // Si es un gol, devuelve el jugador; si no, `nil`.
            if case let .gol(_, jugador, _) = evento { jugador } else { nil }
        }
    }
 
    public func minutosDeGolDe(_ jugador: Jugador) -> [Int] {
        var minutos: [Int] = []   // aquí se van acumulando los minutos
        eventos.forEach { evento in   // la closure se ejecuta una vez por evento
            // Si el evento NO es un gol, sale de esta vuelta (el `return` sale solo de la closure).
            guard case let .gol(minuto, autor, _) = evento else { return }
            // Si el gol es de ese jugador, apunta el minuto (la closure captura `minutos`).
            if autor == jugador { minutos.append(minuto) }
        }
        return minutos
    }

Y las cuatro líneas nuevas de main.swift, en pasos. Van debajo del comentario // Closures: …, encima de repetir(2); lo demás queda como estaba:

Torneo/Sources/torneo-cli/main.swift
// Closures: filtrar eventos y sacar goleadores.
// La closure final decide qué eventos pasan; `.map(describir)` pasa cada uno a texto.
let lasTarjetas = alFinal.eventos { if case .tarjeta = $0 { true } else { false } }
print("Tarjetas: \(lasTarjetas.map(describir))")
// `antesDelMinuto(45)` devuelve la closure que hace de filtro.
let primeraMitad = alFinal.eventos(antesDelMinuto(45))
print("Primera mitad: \(primeraMitad.count) eventos")
// `goleadores()` da un jugador por gol; `\.nombre` se queda con el nombre de cada uno.
let quienesMarcaron = alFinal.goleadores()
print("Goleadores: \(quienesMarcaron.map(\.nombre))")
let minutosDeAna = alFinal.minutosDeGolDe(ana)
print("Minutos de gol de Ana: \(minutosDeAna)")
// (aquí siguen `repetir(2)`, `contador()` y `alResolver`, sin cambios)

En la salida, Tarjetas: ["30\' Tarjeta amarilla para Iván"] lleva comillas y el apóstrofo escapado: así imprime Swift un [String]. Para el aspecto de Kotlin, usa .joined(separator: ", ").

Los tests

ClosuresTests.swift no lo escribes: son ocho comprobaciones de las funciones de esta lección y de la anterior, del mismo tipo que las demás. Se rellena 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 el fichero, pulsar el botón, seleccionar todo, pegar.

  1. En el Project navigator, clic derecho sobre Tests → TorneoTests → New File from Template… → Swift File → Next → ClosuresTests → Create. (Plan B: touch Tests/TorneoTests/ClosuresTests.swift desde ~/Marcador-Swift/Torneo; touch crea un fichero vacío con ese nombre.)
  2. Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
  3. Haz clic dentro de ClosuresTests.swift, selecciona todo con Cmd+A y pega con Cmd+V.
ClosuresTests.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:
Tarjetas: ["30\' Tarjeta amarilla para Iván"]
Primera mitad: 2 eventos
Goleadores: ["Ana", "Iván", "Ana"]
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2
Contador: 1, 2, 3
Ganó Rayo FC
Partidos vistos desde `torneo`: 1
  • Test. xcrun swift test termina con Test run with 42 tests in 6 suites passed: los 34 anteriores y los 8 de ClosuresTests.swift.
  • Provocado. En minutosDeGolDe cambia autor == jugador por autor != jugador: xcrun swift test falla (en ClosuresTests, el test de los minutos con return en la closure). Devuélvelo a como estaba.
  • Sabes decir qué descarta compactMap, y por qué un return dentro de forEach no sale de minutosDeGolDe.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Dentro de eventos.forEach { evento in … } escribes return para saltar un evento. ¿Qué hace en Swift?

  2. 2.Con [.cafe, .tostada, .cafe] y una closure que devuelve el extra de las tostadas y nil en los cafés, ¿qué hace compactMap con los nil?

  3. 3.esGol es una constante cuyo valor es una closure de tipo (EventoDePartido) -> Bool. ¿Qué hace eventos.filter(esGol)?

  4. 4.Partido tiene una propiedad eventos y una función eventos(_:). ¿Cómo elige Swift entre las dos?

  5. 5.quienesMarcaron.map(\.nombre) recibe una lista de Jugador. ¿Qué devuelve?

Faltan 5 de 5