Enums en Swift: mucho más que una lista de constantes
Arturo Rivas Arias
🃏 Si vienes de Objective-C, Java o cualquier lenguaje donde un enum es básicamente un entero con nombre bonito, Swift va a sorprenderte. Los enums de Swift son tipos de datos muy completos: pueden transportar información propia en cada caso, tener métodos y propiedades computadas, conformar protocolos y modelar estructuras recursivas. Son una de las herramientas más expresivas del lenguaje, y no usarlas en todo su potencial es dejar sobre la mesa una ventaja real de diseño.
🏷️ El punto de partida son los valores asociados. Cada caso de un enum puede llevar su propio contenido con un tipo distinto al del resto. Piensa en el ciclo de vida de una descarga de audio en una app de música: el estado de carga no es un booleano ni un conjunto de opcionales flotando en el ViewModel, es una sola fuente de verdad que expresa exactamente qué ocurre en cada momento.
enum DownloadState {
case idle
case downloading(progress: Double, trackId: String)
case paused(bytesReceived: Int, trackId: String)
case completed(localURL: URL)
case failed(reason: Error)
}
Cada caso captura exclusivamente lo que necesita. .completed no tiene progress. .failed no tiene localURL. No hay opcionales innecesarios, no hay propiedades que solo tienen sentido para algunos estados. El tipo hace que los estados inválidos sean irrepresentables.
⚖️ Cuando necesitas mapear un enum a un tipo primitivo, los raw values son la respuesta. Un caso habitual es trabajar con respuestas de una API REST donde el servidor devuelve String fijos en JSON. Al conformar el enum a Codable, la síntesis automática usa el raw value como clave de decodificación, sin necesidad de escribir un init(from:) a mano.
enum AudioQuality: String, Codable {
case low = "low_128"
case standard = "standard_256"
case high = "high_320"
case lossless = "lossless_flac"
}
struct StreamingPreferences: Codable {
let quality: AudioQuality
let normalizeVolume: Bool
}
Un JSON con "quality": "high_320" se decodifica directamente a .high sin ningún código adicional. Y si el servidor envía un valor desconocido, la inicialización falible de rawValue te permite manejarlo de forma controlada.
🧠 El compilador de Swift impone que los switch sobre enums sean exhaustivos. Esto no es solo una característica de ergonomía: es una garantía de diseño que escala con el tiempo. Cuando añades un caso nuevo a un enum, el compilador señala inmediatamente que ningún case del switch lo contempla. Lo que en otros lenguajes sería un bug silencioso en producción, en Swift es un error de compilación explícito.
func iconName(for state: DownloadState) -> String {
switch state {
case .idle: "arrow.down.circle"
case .downloading: "arrow.down.circle.dotted"
case .paused: "pause.circle"
case .completed: "checkmark.circle.fill"
case .failed: "exclamationmark.circle"
}
}
Si mañana añades un caso .queued al enum, este switch dejará de compilar, pero puedes estar tranquilo: el compilador te llevará de la mano hasta cada lugar que necesita actualizarse.
🧩 Los enums pueden tener métodos y propiedades computadas, lo que los convierte en tipos activos, no meros contenedores pasivos. La lógica que depende del estado vive en el propio tipo en lugar de dispersarse por el código que lo consume.
extension DownloadState {
var isActive: Bool {
switch self {
case .downloading, .paused: return true
default: return false
}
}
var trackId: String? {
switch self {
case .downloading(`_, let id): return id
case .paused(_, let id): return id
default: return nil
}
}
}
Ahora puedes escribir state.isActive en lugar de repetir la comparación por toda la vista o el ViewModel. La lógica tiene un único punto de definición.
🌳 Existe una categoría especial de enums que merece atención propia: los enums recursivos, habilitados por la palabra clave indirect. Un enum es recursivo cuando alguno de sus casos referencia al propio tipo como valor asociado. El problema que resuelve indirect es estructural: los enums son tipos valor y Swift necesita conocer su tamaño en tiempo de compilación. Si un caso contiene el mismo tipo, el tamaño sería infinito. indirect rompe ese ciclo indicándole al compilador que almacene ese caso en el heap mediante una referencia, en lugar de hacerlo de forma inline.
indirect enum JSONValue {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([JSONValue])
case object([String: JSONValue])
}
Con este tipo puedes representar cualquier documento JSON válido con plena seguridad de tipos. La recursividad de array y object es exactamente la que hace que JSON sea un formato anidable de forma arbitraria.
🔬 Bajo el capó, cuando marcas un caso como indirect, Swift introduce una indirección mediante un puntero hacia el heap. El enum en sí almacena una referencia, no el valor completo. El coste es real —una asignación de heap adicional, un nivel extra de indirección en los accesos— pero es el único mecanismo que hace posible la recursión sin sacrificar la semántica de tipo valor externamente. Una copia de un enum indirect sigue funcionando como una copia: mutar una instancia no afecta a la otra.
func countNodes(_ value: JSONValue) -> Int {
switch value {
case .null, .bool, .number, .string:
return 1
case .array(let elements):
return 1 + elements.reduce(0) { $0 + countNodes($1) }
case .object(let pairs):
return 1 + pairs.values.reduce(0) { $0 + countNodes($1) }
}
}
La función es recursiva sobre una estructura recursiva. El compilador puede razonar sobre todos los casos posibles y garantizar que la función es exhaustiva, algo que con tipos basados en herencia requeriría un esfuerzo considerablemente mayor.
🏗️ Uno de los usos más productivos de los enums en la práctica diaria es el modelado del estado de una vista. En lugar de combinar flags booleanos con opcionales —isLoading: Bool, data: [Item]?, error: Error?— puedes expresar todo el ciclo de vida de una pantalla en un único tipo.
enum ContentState<T> {
case idle
case loading
case loaded(T)
case empty
case error(Error)
}
@Observable
final class PodcastListViewModel {
private(set) var state: ContentState<[Podcast]> = .idle
func fetchPodcasts() async {
state = .loading
do {
let results = try await podcastService.fetchAll()
state = results.isEmpty ? .empty : .loaded(results)
} catch {
state = .error(error)
}
}
}
La vista recibe un único valor que no puede estar en un estado contradictorio o inconsistente. No existe la combinación isLoading = true junto a data = [...]. Los estados imposibles no son representables, y eso elimina de golpe un montón de bugs de sincronización.
🎯 La regla para saber cuándo usar indirect es sencilla: úsalo cuando la estructura de tus datos sea inherentemente recursiva, nunca antes. No es una optimización ni una forma de optimización temprana. Es la respuesta a un problema concreto: necesito que este tipo se refiera a sí mismo. Marcarlo en casos específicos, en lugar de a nivel de todo el enum, es la forma más precisa de aplicarlo, ya que el coste de la indirección solo se paga donde realmente hace falta.
👨💻 Los enums de Swift son la manifestación más directa del principio de hacer que los estados inválidos sean irrepresentables. Desde un enum simple que modela los palos de una baraja hasta una estructura recursiva que representa un árbol de sintaxis abstracta, el mecanismo es el mismo: el tipo expresa exactamente lo que puede existir, y el compilador se encarga de que no puedas salirte de ese contrato.