Saltar al contenido
K
Swift: el lenguajePaso a paso45 min de lectura · 8 min de máquina Básico

Colecciones y key paths

Al terminar: Usar array, set y diccionario, recorrerlos, transformarlos y ordenarlos con key paths.

Al terminar tendrás la tabla de posiciones de Marcador también en Swift: Rayo FC primero con 6 puntos, Toros con 3 y Lobos con 0, con las mismas columnas y el mismo texto que imprimió tu versión de Kotlin.

Esta lección es un repaso con orden. En las anteriores ya usaste algunas de estas piezas (map, filter, reduce, Set, diccionarios, flatMap y las rutas \.nombre) sin pararte a explicarlas. Hoy se explican bien: las tres colecciones de Swift (lista, conjunto y diccionario), las operaciones que las encadenan y las rutas de propiedad (key paths). Con ellas escribes la tabla; las propiedades calculadas que aparecen en el paso (diferencia) se explican a fondo en la lección siguiente.

Tres colecciones

SwiftKotlinSe pregunta por…Repetidos
[Partido] (Array)List<Partido>posición: lista[0]sí
Set<Equipo>Set<Equipo>pertenenciano
[Equipo: Int] (Dictionary)Map<Equipo, Int>clave: mapa[clave]valores sí, claves no

Tres diferencias con Kotlin. No hay versión «mutable» y otra «de solo lectura»: el tipo es el mismo, con let no cambia y con var, sí («Si asignas un array, un set o un dictionary a una constante, esa colección es inmutable», dice la documentación). Son tipos de valor: copiar un array copia el array (struct frente a class). Y el orden de un Set y de un Dictionary cambia entre ejecuciones: si necesitas un orden, usa sorted.

Las operaciones que encadenan

Ninguna cambia la colección original: devuelven una nueva. Reciben closures (Closures): filter { $0 > 1 }.

SwiftQué haceKotlinCon [3, 1, 2, 1]
map { … }transforma cada elementomap[6, 2, 4, 2] con $0 * 2
filter { … }se queda con los que cumplenfilter[3, 2] con $0 > 1
reduce(0) { total, g in … }junta todo en un valor, con valor inicialfold7 con reduce(0, +)
compactMap { … }transforma y descarta los nilmapNotNullver abajo
count { … }cuenta los que cumplencount2 con $0 == 1
Dictionary(grouping:by:)agrupa en un diccionariogroupByver abajo

reduce de Swift siempre recibe el valor inicial, así que en una colección vacía devuelve ese valor y no falla (el reduce de Kotlin lanzaba una excepción). reduce(0, +) funciona porque + es una función de dos números y se puede pasar tal cual.

Un diccionario tiene además tres cosas propias: values (solo los valores), keys (solo las claves) y, al recorrerlo o ordenarlo, cada elemento es una pareja con key (la clave) y value (el valor). Como un diccionario no tiene orden, para recorrerlo en orden se usa sorted, que devuelve un array de parejas. En for (nombre, edad) in …, los paréntesis abren cada pareja en dos constantes, una por cada parte (es una tupla, que se explica un poco más abajo).

ejemplo mínimo: colecciones (con comentarios)
let goles = [3, 1, 2, 1]
print(goles.map { $0 * 2 })            // [6, 2, 4, 2]
print(goles.filter { $0 > 1 })         // [3, 2]
print(goles.reduce(0, +))              // 7
 
// `Int("diez")` devuelve `Int?`: nil si el texto no es un número (Opcionales).
// `compactMap` convierte y filtra a la vez: se queda con los que no son nil.
let textos = ["10", "diez", "7"]
print(textos.map { Int($0) })          // [Optional(10), nil, Optional(7)]
print(textos.compactMap { Int($0) })   // [10, 7]
 
// `first` de un texto es un `Character?`; `?? "?"` da un valor por defecto.
let porInicial = Dictionary(grouping: ["Ana", "Iván", "Alba"], by: { $0.first ?? "?" })
print(porInicial["A"] ?? [])           // ["Ana", "Alba"]
// Buscar en un diccionario devuelve un opcional; `default:` es «y si no está, esto».
print(porInicial["Z", default: []])    // []: la clave no existe
 
// Un diccionario de edades: values, keys y recorrerlo en orden.
let edades = ["Ana": 30, "Iván": 25]
print(edades.values.reduce(0, +))                 // 55: values, solo los valores
print(edades.keys.sorted())                       // ["Ana", "Iván"]: keys, solo las claves
for (nombre, edad) in edades.sorted(by: { $0.key < $1.key }) {
    print(nombre, edad)                           // cada elemento es una pareja: .key y .value
}
[6, 2, 4, 2]
[3, 2]
7
[Optional(10), nil, Optional(7)]
[10, 7]
["Ana", "Alba"]
[]
55
["Ana", "Iván"]
Ana 30
Iván 25

Rutas de propiedad (key paths)

Hasta ahora escribías { $0.nombre } para sacar la propiedad de cada elemento. Una ruta de propiedad (key path) es esa misma idea como un valor: \.nombre significa «la propiedad nombre de lo que llegue».

ejemplo mínimo: key paths (con comentarios)
struct Jugador {
    let nombre: String
    let dorsal: Int?
}
let jugadores = [Jugador(nombre: "Ana", dorsal: 9), Jugador(nombre: "Luis", dorsal: nil)]
 
print(jugadores.map { $0.nombre })   // ["Ana", "Luis"]
print(jugadores.map(\.nombre))       // ["Ana", "Luis"]: lo mismo, más corto
print(jugadores.compactMap(\.dorsal))// [9]: quita el nil de Luis
["Ana", "Luis"]
["Ana", "Luis"]
[9]

Se pueden encadenar (\.equipo.nombre) y guardar en una constante. En Kotlin el equivalente es Jugador::nombre.

Juntar las piezas: una extensión que agrupa y no repite

La tabla de posiciones usa casi todo lo anterior a la vez, y alguna pieza más. Antes de escribirla, las piezas juntas en otro dominio: los kilómetros que suma cada ciudad en una lista de viajes. Las piezas nuevas:

  • extension Array where Element == Viaje: añade métodos solo a los arrays de un tipo concreto, sin tocar Array (Protocolos y extensiones, con una condición). Ya pegaste una igual, goleadores, en some y any.
  • flatMap { … }: la closure devuelve un array por elemento y flatMap los une en uno. Aquí cada viaje da dos apariciones, una por cada ciudad.
  • Tupla con nombres: (ciudad: "Cali", km: 390) se lee con .ciudad y .km. Es el Triple de Kotlin, con etiquetas (y es lo que descomponía el for (nombre, edad) de arriba).
  • vistas.insert(ciudad).inserted: insert en un Set dice con inserted si el elemento era nuevo; con eso, el bucle se queda con cada ciudad una sola vez y conserva el orden. ciudades + porCiudad.keys une el array de ciudades con las claves del diccionario.
ejemplo mínimo: extensión de Array, flatMap a tuplas con nombre, grouping y un Set que no repite (con comentarios)
// Un tramo entre dos ciudades.
struct Viaje {
    let origen: String
    let destino: String
    let km: Int
}
 
// Solo los arrays de Viaje reciben este método.
extension Array where Element == Viaje {
    func kmPorCiudad(ciudades: [String] = []) -> [String: Int] {
        // Cada viaje da DOS apariciones, una por ciudad: flatMap une los arrays en uno.
        let apariciones = flatMap { viaje in
            [
                (ciudad: viaje.origen, km: viaje.km),     // tupla con nombres: se lee con .ciudad y .km
                (ciudad: viaje.destino, km: viaje.km),
            ]
        }
        let porCiudad = Dictionary(grouping: apariciones, by: \.ciudad)   // ciudad → sus apariciones
 
        // insert(…).inserted es true solo la primera vez: así no se repite ninguna ciudad.
        var vistas = Set<String>()
        var todas: [String] = []
        for ciudad in ciudades + porCiudad.keys {          // + une el array con las claves
            if vistas.insert(ciudad).inserted { todas.append(ciudad) }
        }
 
        var resultado: [String: Int] = [:]
        for ciudad in todas {
            let suyas = porCiudad[ciudad] ?? []            // una ciudad sin viajes: lista vacía
            resultado[ciudad] = suyas.map(\.km).reduce(0, +)
        }
        return resultado
    }
}
 
let viajes = [Viaje(origen: "Cali", destino: "Pasto", km: 390), Viaje(origen: "Cali", destino: "Buga", km: 75)]
let tabla = viajes.kmPorCiudad(ciudades: ["Tuluá"])
print(tabla["Cali"] ?? 0, tabla["Tuluá"] ?? -1)   // 465 0: Tuluá sale aunque no tenga viajes
465 0

Reto · Ejercicio: goleadores con reduce, sorted y grouping

En tu Playground, parte de este diccionario de goles por jugador:

enunciado: datos del ejercicio
let goles = ["Ana": 3, "Iván": 2, "Marta": 2, "Luis": 1]
  1. Imprime el total de goles, juntando todos los valores del diccionario en un solo número.
  2. Imprime la lista de nombres de más a menos goles; si dos empatan, van en orden alfabético. Pista: la ordenación puede recibir una comparación que mira dos elementos a la vez, cada uno con su clave y su valor.
  3. Agrupa los nombres por número de goles e imprime cada grupo en una línea, con los goles delante y los nombres ordenados, de más a menos goles.

Lo que debe salir:

8
["Ana", "Iván", "Marta", "Luis"]
3 ["Ana"]
2 ["Iván", "Marta"]
1 ["Luis"]
Ver solución
solución: goleadores
let goles = ["Ana": 3, "Iván": 2, "Marta": 2, "Luis": 1]
 
print(goles.values.reduce(0, +))   // 8
 
let ranking = goles
    .sorted { a, b in
        if a.value != b.value { return a.value > b.value }
        return a.key < b.key
    }
    .map(\.key)
print(ranking)   // ["Ana", "Iván", "Marta", "Luis"]
 
let porGoles = Dictionary(grouping: goles.keys, by: { goles[$0] ?? 0 })
for (cantidad, nombres) in porGoles.sorted(by: { $0.key > $1.key }) {
    print(cantidad, nombres.sorted())   // 3 ["Ana"], luego 2 ["Iván", "Marta"], luego 1 ["Luis"]
}

Con una closure de dos parámetros, el desempate cabe en una pasada. El diccionario se ordena al imprimirlo, no al crearlo.

Paso 1 · Crea Tabla.swift

Objetivo. Un fichero nuevo en tu paquete Torneo con el struct que representa una fila de la tabla y con tablaDePosiciones, una función sobre [Partido] que devuelve una fila por equipo, ordenada. Compila, y todavía no se imprime.

Torneo
Sources
Torneo
Tabla.swiftse crea
Lo que tocas ahora, en el Project navigator de Xcode con el paquete Torneo abierto.

Los ficheros viven en ~/Marcador-Swift/Torneo.

Crea el fichero.

  1. En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega Sources, clic derecho sobre la carpeta Torneo (la de dentro de Sources) → New File from Template… → Swift File → Next, escribe Tabla y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en ~/Marcador-Swift/Torneo, con touch Sources/Torneo/Tabla.swift.
  2. Comprueba que el fichero quedó dentro de Sources/Torneo.

Escribe su contenido. Primero, un struct con ocho datos guardados y uno calculado (diferencia): una propiedad calculada no guarda nada y se evalúa cada vez que se lee, como cantidadDeJugadores en struct frente a class. La lección siguiente las explica a fondo.

Intenta escribirlo tú. Todo el código de arriba te sirve: es el del ejemplo de los viajes con otros datos.

  • FilaDePosicion: un struct público que se pueda comparar con == (adopta Equatable), con las propiedades públicas de solo lectura equipo, jugados, ganados, empatados, perdidos, golesAFavor, golesEnContra y puntos, y una calculada, diferencia: los goles a favor menos los goles en contra.

  • tablaDePosiciones: un método público que solo tienen las listas de partidos. Recibe:

    • con la etiqueta equipos, los equipos que deben salir aunque no hayan jugado (ninguno si no se dan);
    • con la etiqueta reglamento, el reglamento de puntos (el de liga si no se da).

    Devuelve las filas de la tabla:

    • cada partido cuenta dos veces, una por equipo, con sus goles a favor y en contra;
    • cada equipo sale una sola vez, primero los dados y después los demás, con sus partidos jugados, ganados, empatados y perdidos, sus goles a favor y en contra y sus puntos según el reglamento;
    • se ordena por puntos y, si empatan, por diferencia de goles, después por goles a favor y por último por nombre, con el Ranking de Genéricos en Swift. Aquí no hace falta Comparable: el Ranking recibe la comparación de desempate que le das.

Los tests de la lección siguiente exigen ese nombre y esas etiquetas. Si te ayuda, empieza imprimiendo cada paso. La función sigue cinco pasos, como en Kotlin: flatMap, Dictionary(grouping:by:) con \.equipo, un Set que une los equipos sin repetir, map con count y reduce para cada fila, y el Ranking.

Qué se escribe y dónde. Todo en Tabla.swift: FilaDePosicion arriba y la extensión debajo.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/Tabla.swift
// Una fila de la tabla de posiciones. Un struct con propiedades guardadas y una
// CALCULADA (`diferencia`): no ocupa espacio, se evalúa cada vez que se lee.
public struct FilaDePosicion: Equatable {
    public let equipo: Equipo
    public let jugados: Int
    public let ganados: Int
    public let empatados: Int
    public let perdidos: Int
    public let golesAFavor: Int
    public let golesEnContra: Int
    public let puntos: Int
 
    public var diferencia: Int { golesAFavor - golesEnContra }
}
Torneo/Sources/Torneo/Tabla.swift
// Extensión de `Array` restringida a `Element == Partido`: añade un método a
// `[Partido]` sin tocar Array (la `List<Partido>.tablaDePosiciones()` de Kotlin).
// `equipos` permite que salgan también los que aún no han jugado.
extension Array where Element == Partido {
    public func tablaDePosiciones(
        equipos: [Equipo] = [],
        reglamento: any Reglamento = ReglamentoLiga()
    ) -> [FilaDePosicion] {
        // Cada partido cuenta dos veces: una desde el lado local y otra desde el
        // visitante. Cada elemento es una tupla con nombres: (equipo, aFavor, enContra).
        let apariciones = flatMap { partido in
            [
                (equipo: partido.local, aFavor: partido.golesLocal, enContra: partido.golesVisitante),
                (equipo: partido.visitante, aFavor: partido.golesVisitante, enContra: partido.golesLocal),
            ]
        }
 
        // `Dictionary(grouping:by:)`: agrupa por clave. `\.equipo` es un KEY PATH:
        // una referencia a la propiedad, equivalente a `{ $0.equipo }`. Es
        // Dictionary<Equipo, [(equipo:, aFavor:, enContra:)]> (el `groupBy` de Kotlin).
        let porEquipo = Dictionary(grouping: apariciones, by: \.equipo)
 
        // Un Set no admite repetidos: `insert` dice con `inserted` si el elemento
        // era nuevo. Así se unen los equipos dados y los que ya jugaron, sin repetir.
        var vistos = Set<Equipo>()
        var todos: [Equipo] = []
        for equipo in equipos + porEquipo.keys {
            if vistos.insert(equipo).inserted { todos.append(equipo) }
        }
 
        let filas = todos.map { equipo in
            let marcadores = porEquipo[equipo] ?? []
            return FilaDePosicion(
                equipo: equipo,
                jugados: marcadores.count,
                ganados: marcadores.count { $0.aFavor > $0.enContra },
                empatados: marcadores.count { $0.aFavor == $0.enContra },
                perdidos: marcadores.count { $0.aFavor < $0.enContra },
                golesAFavor: marcadores.map(\.aFavor).reduce(0, +),
                golesEnContra: marcadores.map(\.enContra).reduce(0, +),
                puntos: marcadores.reduce(0) { total, marcador in
                    total + reglamento.puntosPor(golesAFavor: marcador.aFavor, golesEnContra: marcador.enContra)
                }
            )
        }
 
        // La tabla es un Ranking<FilaDePosicion> puntuado por los puntos de la liga.
        return Ranking(
            elementos: filas,
            puntos: \.puntos,
            desempate: { a, b in
                if a.diferencia != b.diferencia { return a.diferencia > b.diferencia }
                if a.golesAFavor != b.golesAFavor { return a.golesAFavor > b.golesAFavor }
                return a.equipo.nombre < b.equipo.nombre
            }
        ).ordenados
    }
}

Compila. En la Terminal, cd ~/Marcador-Swift/Torneo y luego xcrun swift build. Debe terminar con Build complete!. En Xcode, el mismo resultado es Product → Build (Cmd+B).

Paso 2 · Imprimir la tabla

Objetivo. Ver la tabla en la consola. Primero Torneo aprende a darla, después llega el fichero que alinea las columnas y por último el bloque de main.swift que la imprime. Al terminar, xcrun swift run torneo-cli muestra la tabla de Rayo FC, Toros y Lobos.

Torneo.swift: pedir la tabla al torneo

El torneo ya tiene sus partidos, sus equipos y su reglamento: la tabla es llamar a la función que acabas de escribir con esos tres datos. Intenta escribirlo tú: en Torneo (Sources → Torneo), justo debajo del init, un método público tablaDePosiciones() sin parámetros que devuelva la tabla de sus partidos con sus equipos y su reglamento.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/Torneo/Torneo.swift
    public func tablaDePosiciones() -> [FilaDePosicion] {
        partidos.tablaDePosiciones(equipos: equipos, reglamento: reglamento)
    }

Formato.swift: alinear las columnas

Imprimir la tabla es alinear texto en columnas (el "%2d %-8s …".format(…) de Kotlin), y eso no es el tema de la lección: Formato.swift 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).

Ojo: se pega entero y sustituye al que ya tienes. El fichero existe desde Equatable y Hashable y el contenido nuevo trae lo que ya tenía (etiqueta() de Jugador y capitanONinguno de Equipo) más lo nuevo: relleno, aTexto(puesto:) y tablaComoTexto(). Si añadiste algo tuyo a ese fichero, guárdalo antes en otro. Depende de FilaDePosicion y de tablaDePosiciones(), así que cópialo ahora que ya existen.

  1. Abre Formato.swift (Sources → Torneo) con doble clic.
  2. Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
  3. Haz clic dentro del fichero, selecciona todo con Cmd+A y pega con Cmd+V.
Formato.swift

main.swift

En main.swift (Sources → torneo-cli) el bloque nuevo va en medio de dos líneas que ya tienes, y por eso el bloque de abajo las trae: la primera línea (print("Ranking de textos: …"), la última de la lección de Genéricos en Swift) y la última (let torneo = Torneo(…), que hiciste en registrar lanza errores: throws tipado y Result) no se escriben, solo marcan dónde empieza y dónde acaba lo nuevo. Pega, o escribe, lo que hay entre ellas. Lo nuevo usa lobos, alFinal y partidoDeToros, que ya están declarados más arriba. Si lo escribes tú: crea la copa con los tres equipos y registra en ella alFinal, partidoDeToros y un Lobos contra Rayo con un gol de Ana para el Rayo en el minuto 40 (si falla, «No se pudo registrar:» y el error); imprime la tabla como texto. La copa tiene así tres partidos; el torneo de la línea siguiente (el que se guarda después) sigue teniendo dos. Lo que debes ver:

Pos Equipo    PJ  G  E  P  GF GC  DG Pts
 1  Rayo FC   2  2  0  0   3  1   2   6
 2  Toros     2  1  0  1   2  2   0   3
 3  Lobos     2  0  0  2   0  2  -2   0
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo/Sources/torneo-cli/main.swift
print("Ranking de textos: \(porLongitud.ordenados)")
 
// Colecciones: la tabla de posiciones de una jornada completa.
let copa = Torneo(nombre: nombreDelTorneo, equipos: [rayo, toros, lobos])
do {
    try copa.registrar(alFinal)
    try copa.registrar(partidoDeToros)
    try copa.registrar(Partido(local: lobos, visitante: rayo).registrando(.gol(minuto: 40, jugador: ana, equipo: rayo)))
} catch {
    print("No se pudo registrar: \(error.localizedDescription)")
}
print(copa.tablaComoTexto())
 
let torneo = Torneo(nombre: nombreDelTorneo, equipos: [rayo, toros, lobos])

Ahora compila y ejecuta, desde ~/Marcador-Swift/Torneo:

xcrun swift build
xcrun swift run torneo-cli

Punto de control

  • xcrun swift build (desde ~/Marcador-Swift/Torneo) termina sin errores.
  • xcrun swift run torneo-cli imprime, entre otras, estas líneas:
Pos Equipo    PJ  G  E  P  GF GC  DG Pts
 1  Rayo FC   2  2  0  0   3  1   2   6
 2  Toros     2  1  0  1   2  2   0   3
 3  Lobos     2  0  0  2   0  2  -2   0
  • xcrun swift test termina con Test run with 69 tests in 9 suites passed: los mismos de la lección anterior, porque hoy no hay tests nuevos.
  • Provócalo: mueve tu bloque de main.swift por encima de let lobos = …: la tabla sale con dos filas y sin error. Devuélvelo a su sitio.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.Tienes let textos = ["10", "diez", "7"]. ¿Qué devuelve textos.compactMap { Int($0) }?

  2. 2.¿Qué es \.nombre en jugadores.map(\.nombre)?

  3. 3.En main.swift escribes print("antes: \(despues)") y, una línea más abajo, let despues = 5. ¿Qué ocurre?

  4. 4.Tienes let edades = ["Ana": 30, "Iván": 25]. En for (nombre, edad) in edades.sorted(by: { $0.key < $1.key }), ¿qué es cada elemento que ordena sorted?

  5. 5.En var vistos = Set<Equipo>(), ¿qué indica vistos.insert(equipo).inserted?

Faltan 5 de 5