Swift – Enumeraciones

En esta ocación aprenderemos sobre las enumeraciones (enum), para qué son útiles, su implementación y matices de uso. Como siempre todo esto lo veremos a través de ejemplos.

Las enumeraciones usualmente se definen como un conjunto de datos de un mismo tipo que agrupa valores que se relacionan entre sí. Pero esta definición no es del todo acertada en Swift ya que en este lenguaje las enumeraciones son mucho más potentes y flexibles.

En Swift las enumeraciones no necesitan proporcionar un valor para cada caso, ya que el compilador lo infiere. Al mismo tiempo que los valores de estos casos pueden ser de distintos tipos de datos, es decir, cada uno de estos valores pueden ser de un tipo de dato distinto sin que esto sea contemplado como un error.

Algo realmente único en Swift es que las enumeraciones adoptan muchas características que tradicionalmente solo son soportadas por las clases. Me refiero a las propiedades computadas (computed properties) y a los métodos de instancia, ambos nos permiten añadir información extra y funcionalidades para con los casos que la enumertación expone.

Las enumeraciones por ser flexibles, también se les pueden definir inicializadores a favor de proporcionar un valor inicial a cada caso, se pueden expandir (mediante extensions) para ampliar su funcionalidad más allá de su implementación original, y pueden también apoyarse en los protocolos para proporcionar funcionalidades estándar.

Sintaxis

La sintaxis de una enumeración es bien sencilla:

enum <EnumName> {
    
    case <caseName>
    
}

La palabra clave enum le informa al compilador que estamos definiendo una enumeración, asociamos un nombre a esta (EnumName) y dentro de las llaves declaramos los distintos casos (CaseName) que se agruparán semánticamente.

En ejemplo de enumeración pudiera ser:

enum CardinalDirection {
    
    case north
    case south
    case east
    case west
    
}

Aquí hemos declarado una enumeración de nombre CardinalDirection en la cual agrupamos los puntos cardinales

¿Qué es una enumeración?

Luego de ver el ejemplo anterior es mucho más evidente y sencillo repetir lo que ya hemos comentado. Pero vamos poco a poco.

La razón de ser más básica de las enumeraciones es agrupar los estados que cierto evento puede adoptar. No confundir con las estructuras o clases, mediante las cuales definimos entidades: una persona, una mascota, un avión, un modelo de datos, etc.

En una enumeración podemos agrupar por ejemplo el estado marital de esa estructura o clase que define la entidad persona. En esta enumeración se agruparían los distintos casos que dicho evento, dígase: soltero, casado, divorciado y viudo (en algunos países más).

Esta enumeración luciría así:

enum MaritalStatus {
    
    case single
    case married
    case widowed
    case divorced
    
}

Luego esta serviría de complemento a la siguiente estructura de ejemplo donde definimos la entidad Person:

struct Person {
    
    enum MaritalStatus {
        
        case single
        case married
        case widowed
        case divorced
        
    } // MaritalStatus
    
    var maritalStatus: MaritalStatus = .single
    
    let name: String
    let middleName: String?
    let lastName: String

} // Person

y como toda persona tiene un estado marital pues la enumeración nos ayuda a definir esto para cada ciudadano. Luego podriamos hacer algo así:

struct Person: CustomStringConvertible {
    
    enum MaritalStatus {
        
        case single
        case married
        case widowed
        case divorced
        
    } // MaritalStatus

    let name: String
    let lastName: String
    var maritalStatus: MaritalStatus
    
    init(name: String, lastName: String, maritalStatus: MaritalStatus = .single) {
        
        self.name = name
        self.lastName = lastName
        self.maritalStatus = maritalStatus
        
    } // init

    var description: String {
        
        return "Usuario: \(name) \(lastName), estado marital: \(maritalStatus)"
        
    } // description
    
} // Person

let carlos = Person(name: "Carlos", lastName: "Guevara")
let alejandro = Person(name: "Alejandro", lastName: "Núñez", maritalStatus: .married)

let users = [carlos, alejandro]

users.map { user in
    
    print(user)
    
}

La salida en pantalla de este código sería:

Usuario: Carlos Guevara, estado marital: single
Usuario: Alejandro Núñez, estado marital: married

Aquí vemos como la enumeración se integra dentro de la estructura, estableciendo los distintos valores que puede adoptar el estado marital de una persona. Constituyendo un campo más dentro de un formulario gráfico desde el cual, y mediante un combobox, el usuario podría rellenar sus datos.

Nota: En este ejemplo me he flipado un poco pero es solo para que se vea donde entran las enumeraciones en un ejemplo un poco más complejo y cercano a la vida real. Si no entiendes algo no te preocupes o deja tu duda en la seción de comentario, no obstante todo lo que aplicamos aquí se explica en otros artículos del sitio.

El nombre de las Enumeraciones y sus Casos

Tengamos en cuenta que cada declaración de una enumeración define un nuevo tipo de dato, y al igual que otros tipos en Swift, su nombre debe comenzar con una letra mayúscula mientras que los casos los escribimos en minuscula, y todos en singular.

Los nombres se establecen en singular porque una enumeración aunque agrupa varios valores luego de instanciada solo puede establecerce en uno. Es decir que una enumeración nunca puede representar más de un valor al mismo tiempo, y también resulta más legible:

Planet.tierra

a esta otra alternativa en plural:

Planets.tierra

Cuando leemos la primera decimos: «Planeta Tierra» y en la segunda: «Planetas Tierra», es bien clara la diferencia, ¿cierto?

Como también hemos podido observar los casos se definen con la palabra clave case dentro de las llaves del enum. Estos pueden ir en cada línea o en una sola separados por comas:

enum Planet {
    
    case mercurio, venus, tierra, marte, júpiter, saturno, urano, neptuno
    
} // enum

Luego podemos hacer referencia a estos casos usando la sintaxis de punto como hemos visto al inicio de esta sección, de esta manera:

let planetChosenByTheUser = Planet.marte

En este ejemplo creamos una constante donde almacenamos el planeta que el usuario ha elegido, luego de aquí le mostramos información sobre este o mostrarle imagenes del mismo.

Como ven la sintaxis es bien simple y sigue este patrón:

<NombreEnumeración>.<caso>

Pero como Swift es un lenguaje moderno y realmente genial, la inferencia de tipos llega también se aplica a las enumeraciones. Si tenemos una variable donde almacenamos el planeta en donde nació el usuario, asumiendo un futuro donde esto fuese posible, la declararíamos así:

enum Planet {
    
    case mercurio, venus, tierra, marte, júpiter, saturno, urano, neptuno
    case desconocido
    
} // enum

let planetOfBirth = Planet.desconocido

A nuestra enumeración le hemos agregado un nuevo caso (Desconocido), que sería el caso por defecto cuando aún no se conoce en que planeta nació el usuario. Luego a la hora de asignar un valor a esta variable podríamos hacer lo siguiente:

planetOfBirth = .marte

donde obviamos el nombre de la enumeración y mediante el operador de punto (.) accedemos al caso deseado. Esto es posible gracias a que el compilador de Swift ya conoce que la variable planetOfBirth es de tipo Planet, y por ende lo infiere.

Valores Raw

Para establecer que una enumeración va a tener valores en bruto asociados a cada caso lo primero que debemos hacer es establecerlos como anotaciones de tipo al momento de la definición.

enum Planet: Int {
    
    case mercurio = 1, venus, tierra, marte, júpiter, saturno, urano, neptuno
    case desconocido = 0
    
} // enum

Mediante la anotación de tipo definimos que el tipo raw (o valor en bruto) de la enumeración será de tipo Int. Luego de esto asignamos al primer caso el valor de 1, y sin necesidad de ser explicitos el resto de casos toman los valores siguientes, es decir 2, 3, 4, etc.

En el último caso (Desconocido) volvemos a ser explicitos para establecer el valor de 0, de lo contrario se secuencia seguiría y este casi tendría un valor de 9, y como ya sabemos no hay un planeta 9 y menos aún se llama Desconocido.

Luego con una enumeración así pudiéramos jugar con esta correlación entre nombre y número dentro del sistema solar, algo así:

var planetOfBirth = Planet.Desconocido

planetOfBirth = .Júpiter

print("\(planetOfBirth) es el planeta número \(planetOfBirth.rawValue) en el sistema solar.")

la salida en pantalla sería:

Júpiter es el planeta número 5 en el sistema solar.

Otro ejemplo pudiera ser esta enumeración de constantes con sus respectivos valores raw:

enum Constantes: Double {

    case π = 3.14159
    case e = 2.71828
    case φ = 1.61803398874
    case λ = 1.30357

} // Constantes

Valores Asociados

Ahora bien, no todos los escenarios son igual de simples, habrán momentos donde los casos de una enumeración no solo necesitarán de una dato numérico o cadena de texto asociado, quizás necesitarán más de uno.

Imaginemos que tenemos la siguiente enumeración:

enum Airport: String {
    
    case madrid = "MAD"
    case cataluña = "BCN"
    case cantabria = "SDR"
    
} // Airport

En esta tenemos tres comunidades autónomas de España y sus aeropuertos principales, y como valor raw asociado el código IATA de los mismos. El problema de este enfoque es que en las comunidades autónomas de Madrid y Cataluña hay más de un aeropuerto, mientras que en Cantabria hay solo uno.

Supongamos entonces que necesitamos representar otros aeropuertos de estas comunidades autónomas. ¿Qué podemos hacer? Una solución bien chapuza pudiera ser definir todos los aeropuertos en el mismo enum, pero tendriamos que agrupar demasiada información en el propio nombre de los casos y no pidieramos acceder a estos de manera independiente.

enum Airportt: String {
    
    case MadridBarajas = "MAD"
    case MadridCuatroVientos = "MCV"
    
    case CataluñaElPrat = "BCN"
    case CataluñaSabadel = "QSA"
    case CataluñaGerona = "GRO"
    
    case CantabriaSantander = "SDR"
    
} // Airport

En este ejemplo perdemos el acceso individual a la comunidad autónoma, y también luce bastante caótico. Como no estamos aquí para hacer chapuzas y Swift nos brinda opciones pues abogamos por los valores asociados.

El uso de valores asociados en un enum luce así:

enum Airport {
    
    case madrid(airport: String, iata: String)
    case cataluña(airport: String, iata: String)
    case cantabria(airport: String, iata: String)

} // Airport

Los valores los asociamos a cada caso mediante parámetros, a cada comunidad autónoma le pasamos dos valores de tipo String. En el primero pasamos el nombre del aeropuerto y en el segundo el código IATA.

Este código funciona y está bien, solo que hemos perdido la posibilidad de almacenar valores raw. Exacto, cuando utilizamos valores asociados no podemos utilizar valores raw, y por ende hemos perdido la posibilidad de imprimir el nombre de la comunidad autónoma. Veamos:

enum Airport {
    
    case madrid(airport: String, iata: String)
    case cataluña(airport: String, iata: String)
    case cantabria(airport: String, iata: String)

} // Airport

let airport = Airport.cantabria(airport: "Santander", iata: "SDR")

print("Airport: \(airport)")

La salida de este código sería:

Airport: cantabria(airport: "Santander", iata: "SDR")

Coincidencia de Patrones

Como podemos observar la salida nos devuelve de una todos los datos relacionados con este caso. ¿Qué sucede si queremos acceder de manera independiente a cada uno de estos? Pues que lo podemos hacer, podemos acceder a todos menos al nombre de la enumeración, es decir al nombre de la comunidad autónoma.

¿Cómo lo hacemos? Pues mediante la coincidencia de patrones, ya sea con un bloque switch o conjugando un bloque if con la palabra clave case. De esta manera:

enum Airport {
    
    case madrid(airport: String, iata: String)
    case cataluña(airport: String, iata: String)
    case cantabria(airport: String, iata: String)

} // Airport

let airportSelectedByUser = Airport.cantabria(airport: "Santander", iata: "SDR")

if case Airport.cantabria(let airport, let iata) = airportSelectedByUser {
    
    print("Comunidad Autónoma: \(airportSelectedByUser), Aeropuerto: \(airport), código IATA: \(iata)")
    
} // if case

El segmento más interesante de este código reside en el bloque if-case, el cual podriamos leer de la siguiente forma:

airportSelectedByUser es igual al caso Airport.cantabria entonces define las constantes airport e iata con sus valores respectivos.

El ejemplo anterior

Recordemos que al igual que un bloque de código if-let el tiempo de vida de la constante está limitado al ámbito del bloque if, es decir al código comprendido dentro de las llaves del mismo.

Nota: En este caso no hemos utilizado un bloque switch porque de hacerlo tendríamos que gestionar todos los casos de la enumeración, ya que cuando trabajamos una enumeración a través de un bloque switch este tiene que ser exhaustivo, tiene que cubrir todos los casos o establecer un default.

La salida en pantalla del código anterior es la siguiente:

Comunidad Autónoma: cantabria(airport: "Santander", iata: "SDR"), Aeropuerto: Santander, código IATA: SDR

Esto aún no es óptimo por varias razones. No solo es que no podemos acceder de manera independiente al nombre de la comunidad autónoma, es que el resto de valores los estamos pasando a través de literales que pueden contener errores, literales que no cambian y que pudieran ser casos de la enumeración tal y como teníamos en un inicio.

Composición

La solución a esto es separar toda la información que deseamos representar en varias enumeraciones:

enum AirportMadrid: String {
    
    case barajas = "MAD"
    case cuatroVientos = "MCV"

} // AirportMadrid

enum AirportCataluña: String {
    
    case elPrat = "BCN"
    case sabadel = "QSA"
    case gerona = "GRO"
    
} // AirportCataluña

enum AirportCantabria: String {
    
    case santander = "SDR"
    
} // AirportCantabria

para luego componer una que se apoye en estas y las utilice como valores asociados. De esta manera:

enum Airport {
    
    case madrid(airport: AirportMadrid)
    case cataluña(airport: AirportCataluña)
    case cantabria(airport: AirportCantabria)

} // Airport

Una implementación así estaría menos propensa a errores ya que no tenemos que utilizar literales para establecer un aeropuerto y sus código IATA. Ahora podríamos implementar nuestro código así:

let airportSelectedByUser = Airport.madrid(airport: .barajas)

if case Airport.madrid(let airport) = airportSelectedByUser {

    print("Comunidad Autónoma: \(airportSelectedByUser), Aeropuerto: \(airport), código IATA: \(airport.rawValue)")

} // if case

Es mucho más seguro pero aún no es perfecto, la salida en pantalla es:

Comunidad Autónoma: madrid(airport: __lldb_expr_3.AirportMadrid.barajas), Aeropuerto: barajas, código IATA: MAD

Propiedades Computadas

Como podemos observar cuando imprimimos airportSelectedByUser nos devuelve nuevamente el objeto completo, y esto no es lo que queremos. Tampoco podemos tener valores raw como ya hemos comentado, así que necesitamos una alternativa.

La forma de eludir la limitante de valores raw cuando trabajamos con valores asociados es mediante una propiedad computada, quedando nuestra enumeración así:

enum Airport {
    
    case madrid(airport: AirportMadrid)
    case cataluña(airport: AirportCataluña)
    case cantabria(airport: AirportCantabria)
    
    var autonomousCommunity: String {

        switch self {

        case .madrid:

            return "Madrid"

        case .cataluña:

            return "Cataluña"

        case .cantabria:

            return "Cantabria"

        } // switch

    } // autonomousCommunity
    
} // Airport

Hemos definido dentro de la enumeración una propiedad computada de nombre autonomousCommunity, dentro de la cual utilizamos un bloque switch asociado a self, asociado a la instancia de Airport.

Siguiendo este enfoque podemos determinar de manera dinámica la comunidad autónoma, ya que al hacer referencia a self accedemos al valor que hemos pasado a nuestra instancia airportSelectedByUser, esto activaría el caso correspondiente dentro del bloque switch y con esto el valor de la propiedad computada sería igual al nombre de la comunidad autónoma que hemos asociado al caso de la enumeración.

Con esta nueva versión de la enumeración, podemos modificar nuestro código a esta versión:

let airportSelectedByUser = Airport.madrid(airport: .barajas)

if case Airport.madrid(let airport) = airportSelectedByUser {

    print("Comunidad Autónoma: \(airportSelectedByUser.autonomousCommunity), Aeropuerto: \(airport), código IATA: \(airport.rawValue)")

} // if case

y ahora la salida en pantalla es:

Comunidad Autónoma: Madrid, Aeropuerto: barajas, código IATA: MAD

Al fin tenemos lo que deseamos, y no necesitamos utilizar literales que den lugar a un eventual error. Tampoco necesitamos tirar de un bloque switch externo para determinar el nombre de la comunidad autónoma, esto introduciría nuevos literales que a su vez representan un retroceso en temas de seguridad.

Iterando sobre los Casos de la Enumeración

En ocaciones es muy útil poder iterar sobre los casos de una enumeración o poder devolver el número de casos definidos. Swift también nos permite esto, mediante el protocolo CaseIterable.

Veamos un ejemplo un poco más sencillo para ejemplificar esto:

enum Beverage: CaseIterable {
    
    case coffee
    case tea
    case juice

} // Beverage

let ofertas = Beverage.allCases.count

print("¡Tenemos \(ofertas) tipos de bebidas disponibles!")

Aquí tenemos una enumeración en representación de las bebidas que ofertamos en nuestra cafetería. Hemos declarado también que esta enumeración implementará el protocolo CaseIterable.

Esto lo hacemos ya que nuestra oferta puede variar en número, y por ende necesitamos conocer la cantidad de casos definidos (ofertas de bebidas), este dato lo optenemos mediante count, en esta línea:

let ofertas = Beverage.allCases.count

es decir que en ofertas se almacena la cantidad de casos que se definen en la enumeración. La salida en pantalla es:

¡Tenemos 3 tipos de bebidas disponibles!

Ahora, ¿qué tal si a partir de estos casos queremos crear el menú?

enum Beverage: Double, CaseIterable {
    
    case coffee = 2.5
    case tea = 1.5
    case juice = 5.0
    
    var name: String {
        
        switch self {
            
        case .coffee:
            
            return "Café"
            
        case .tea:
            
            return "Té"
            
        case .juice:
            
            return "Jugo"

        } // switch
        
    } // name

} // Beverage

let ofertas = Beverage.allCases.count

print("¡Hoy tenemos \(ofertas) bebidas disponibles!\n")

for bebida in Beverage.allCases {
    
    print("\(bebida.name) ........ precio: $\(bebida.rawValue)")
    
} // for

Pues muy sencillo, junto al protocolo hemos declarado que nuestra enumeración incorporará valores raw de tipo Double, luego mediante la propiedad computada name establecemos el formato de los nombres de cada caso: en mayusculas e incluso lo pudieramos traducir si fuese necesario, etc.

En el bloque for-in nos apoyamos en el arreglo allCases que está compuesto de todos los casos que implementa nuestra enumeración. Luego en cada iteración accedemos al nombre asociado a este caso y al valor raw que en nuestro caso es el precio del producto.

La nueva salida en pantalla sería la siguiente:

¡Hoy tenemos 3 bebidas disponibles!

Café ........ precio: $2.5
Té ........ precio: $1.5
Jugo ........ precio: $5.0

Conclusiones

Las enumeraciones son un recurso muy flexible y de uso más que frecuente; junto a los bloques switch conforman un par sumamente útil y que nos ayudan a estructurar mucho mejor nuestro código.

Se que hay matices un poco abstractos, es inevitable al inicio; son aspectos que van tomando forma en la medida que vamos aprendiendo y haciendo.

Recuerden que Swift by Coding viene de Learn Swift by Coding, que traducido sería: Aprende Swift Programando.

Dicho esto, mientras practiquemos lo que vamos aprendiendo, la propia necesidad nos irá ayudando a entender porque las cosas son como son, y cuando es mejor utilizar una enumeración a una estructura por ejemplo, y viseversa; poco a poco podremos ir colocando todo en el lugar que lleva dentro de nuestro código.

Falta aún mucho por aprender en nuestro camino a convertirnos en iOS Developer. Suscríbete a nuestra lista de correo y síguenos en nuestras redes sociales. Mantente al tanto de todas nuestras publicaciones.

Espero que todo cuanto se ha dicho aquí, de una forma u otra le haya servido de aprendizaje, de referencia, que haya valido su preciado tiempo.

Este artículo, al igual que el resto, será revisado con cierta frecuencia en pos de mantener un contenido de calidad y actualizado.

¡Cualquier duda o sugerencia, ya sea errores a corregir o ejemplos a añadir, será más que bienvenida, necesaria!

Deja una respuesta

Su dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para mejorar su experiencia. Asumiremos que está de acuerdo con esto, pero puede optar por no participar si lo desea. Aceptar Leer Más

RECIBE CONTENIDO SIMILAR EN TU CORREO

RECIBE CONTENIDO SIMILAR EN TU CORREO

Suscríbete a nuestra lista de correo y mantente actualizado con las nuevas publicaciones.

Se ha suscrito correctamente!