Es habitual recorrer un array con enumerated() y llamar index al primer valor de la tupla. Parece lógico: obtenemos un número junto a cada elemento. Pero ese nombre puede esconder una suposición peligrosa, especialmente cuando el código empieza a trabajar con fragmentos de esa colección.
La documentación de enumerated() distingue entre el contador del recorrido y el índice del elemento. El método produce duplas (offset, element): el primer componente empieza en cero y aumenta en cada iteración. No garantiza que podamos utilizarlo para acceder a la colección con un subíndice.
En un Array, ambos valores coinciden porque sus índices son enteros consecutivos que empiezan en cero. Un ArraySlice, en cambio, conserva los índices del array del que procede. Imagina que estamos preparando las últimas etapas de una ruta:
let stages = ["Salida", "Bosque", "Refugio", "Cumbre"]
let remaining = stages[2...]
print(remaining.startIndex) // 2
print(Array(remaining.indices)) // [2, 3]
for (offset, stage) in remaining.enumerated() {
print(offset, stage)
// 0 Refugio
// 1 Cumbre
}
remaining[0] provocaría un error de acceso fuera de rango. Tener dos elementos no significa que sus índices sean 0 y 1. Aquí son 2 y 3. El compilador acepta la expresión porque tanto el contador como el índice de ArraySlice son Int: sus tipos coinciden, aunque representan conceptos diferentes.
Si necesitamos numerar las etapas pendientes, enumerated() encaja perfectamente. El número mostrado corresponde al recorrido actual y podemos utilizar directamente el elemento recibido:
for (offset, stage) in remaining.enumerated() {
print("Etapa pendiente \(offset + 1): \(stage)")
}
Para obtener posiciones válidas, Collection ofrece indices. Si además necesitamos el elemento, podemos combinar ambos recorridos con zip:
for index in remaining.indices {
print(remaining[index])
}
for (index, stage) in zip(remaining.indices, remaining) {
print("Índice original: \(index), etapa: \(stage)")
}
La diferencia resulta todavía más evidente con String. Sus índices son String.Index, mientras que enumerated() sigue proporcionando un Int. Por eso text[offset] no compila. Recorrer text.indices permite acceder a sus caracteres sin asumir que cada uno ocupa una posición entera consecutiva. Tampoco conviene conservar índices después de operaciones que puedan invalidarlos, como ciertas inserciones o eliminaciones.
Si disponemos de una distancia y necesitamos convertirla en un índice, existe index(_:offsetBy:):
let offset = 1
let index = remaining.index(remaining.startIndex, offsetBy: offset)
print(remaining[index]) // Cumbre
Esta operación exige respetar los límites de la colección. Además, avanzar una distancia no siempre supone lo mismo: en colecciones de acceso aleatorio es constante; en otras puede requerir recorrer elementos. Recalcular cada posición desde startIndex dentro de un bucle puede acumular trabajo innecesario. Para recorrer todos los elementos, indices o zip expresan mejor la intención.
En SwiftUI aparece otra distinción: posición e identidad tampoco son equivalentes. Un contador puede servir para mostrar el número de una fila, pero usarlo como id en una lista que cambia hace que la identidad siga a la posición. Al insertar una etapa al principio, las siguientes cambian de número aunque sigan siendo las mismas etapas.
import SwiftUI
struct Stage: Identifiable {
let id: UUID
let name: String
}
struct RouteView: View {
let stages: [Stage]
var body: some View {
List {
ForEach(Array(stages.enumerated()), id: \.element.id) { entry in
Text("\(entry.offset + 1). \(entry.element.name)")
}
}
}
}
Aquí, ForEach identifica cada etapa mediante su identificador estable, que debe conservarse entre actualizaciones. offset solo aporta la numeración visible. La conversión a Array permite usar este patrón también en versiones donde el resultado de enumerated() no satisface los requisitos de colección de ForEach.
Antes de elegir el recorrido, conviene precisar qué necesitamos: el elemento, un número para mostrar, una posición válida o una identidad estable. Nombrar cada valor según su función ayuda a evitar que una coincidencia propia de los arrays se convierta en una suposición sobre todas las colecciones.