Un Recorrido Por Swift

Es costumbre que el primer programa que se crea al aprender un nuevo lenguaje de programación es aquel que imprime la frase «¡Hola, mundo!» en la pantalla. En Swift, esto se puede conseguir con una sola línea de código:

1print("¡Hola, mundo!")
2// Imprime "¡Hola, mundo!"

Si has desarollado anteriormente en C u Objective-C, esta sintaxis te resultará familiar; en Swift, esta línea de código representa un programa como tal. No hace falta importar una biblioteca aparte para contar con funciones como entrada/salida o manejo de cadenas de texto. Todo código escrito en el ámbito (scope) global se utiliza como punto de entrada para el programa, por lo que no necesitamos una función main(). Tampoco hace falta escribir punto y coma al final de cada declaración.

Esta guía te proporciona suficiente información para comenzar a desarrollar código en Swift al enseñarte cómo realizar una variedad de tareas de programación. No te preocupes si hay algo que no entiendes: todo lo presentado en esta guía se explica en detalle en el resto de este libro.

Nota

Para una mejor experiencia, abre este capítulo como un playground en Xcode. Los playgrounds te permiten editar bloques de código y ver el resultado inmediatamente.


Descargar Playground (en inglés)

Valores Sencillos

Usa let para crear una constante y var para crear una variable. No es necesario que conozcamos el valor de una constante durante la compilación, pero tal valor debe asignarse exactamente una única vez. Esto significa que puedes usar constantes para nombrar un valor que solo se define una vez, pero que se usa muchas veces.

1var miVariable = 42
2
3miVariable = 50
4
5let miConstante = 42

Una constante o variable debe ser del mismo tipo que el valor que se le quiera asignar. Sin embargo, no hace falta ser explícitos al respecto. El proporcionar un valor a una variable o constante durante su creación, le permite al compilador inferir el tipo de dicha variable o constante. En el anterior ejemplo, el compilador infiere que miVariable es un entero porque su valor inicial es un entero.

Si el valor inicial no proporcionara suficiente información (o si no hubiera un valor inicial), especificamos el tipo al escribirlo después de la variable, seguido por dos puntos.

1let enteroImplicito = 70
2let doubleImplicito = 70.0
3let doubleExplicito: Double = 70

Experimenta

Crea una constante con un tipo específico Float y un valor de 4.

Los valores nunca son convertidos a un tipo diferente de manera implícita. Si necesitas convertir un valor a un tipo diferente, debes crear –explícitamente–, una instancia del tipo deseado.

1let etiqueta = "El ancho es "
2let ancho = 94
3let etiquetaAncho = etiqueta + String(ancho)

Experimenta

Remueve la conversión a String en la última línea. ¿Cuál es el error que aparece?

Hay una manera incluso más sencilla de insertar valores en una cadena de texto: Escribe el valor en paréntesis, precedido por un backslash (\). Por ejemplo:

1let manzanas = 3
2let naranjas = 5
3let resumenManzanas = "Tengo \(manzanas) manzanas."
4let resumenFrutas = "Tengo \(manzanas + naranjas) frutas."

Experimenta

Usa \() para insertar una operación con números de coma flotante en una cadena de texto y para incluir el nombre de una persona en un saludo.

Usa tres comillas dobles para cadenas de texto que ocupan más de una línea. La sangría (indentation) al inicio de cada línea de la cadena de texto es removida siempre y cuando concuerde con la sangría de las comillas de cierre. Por ejemplo:

1let cita = """
2He dicho "Tengo \(manzanas) manzanas."
3Y luego he dicho "Tengo \(manzanas + naranjas) frutas."
4"""

Crea arrays y diccionarios usando corchetes ([]) y accede a sus elementos referenciándolos por su índice o su clave en corchetes. Está permitido el uso de coma después del último elemento.

1var listaDeCompras = ["cereal", "frutas", "helado"]
2listaDeCompras[1] = "botella de agua"
3
4var ocupaciones = [
5    "Malcolm": "Capitán",
6    "Kaylee": "Mecánico"
7]
8ocupaciones["Jayne"] = "Relaciones Públicas"

Los arrays crecen automáticamente a medida que agregas elementos.

1listaDeCompras.append("pintura azul")
2print(listaDeCompras)

Para crear un array o un diccionario vacíos, usa la sintaxis de inicialización.

1let arrayVacio: [String] = []
2let diccionarioVacio: [String: Float] = [:]

Si es posible inferir información sobre el tipo de datos, podrás declarar un array vacío así [] y un diccionario vacío así [:]. Por ejemplo, al especificar un nuevo valor para una variable o al pasar un argumento a una función.

1listaDeCompras = []
2ocupaciones = [:]

Flujo de control

Usa if y switch para crear condicionales, y usa for-in, while, y repeat-while para crear bucles. Los paréntesis alrededor de la condición o de la variable del bucle son opcionales. Las llaves alrededor del cuerpo son obligatorias.

1let puntajesIndividuales = [75, 43, 103, 87, 12]
2var puntajeDelEquipo = 0
3
4for puntaje in puntajesIndividuales {
5    if puntaje > 50 {
6        puntajeDelEquipo += 3
7    } else {
8        puntajeDelEquipo += 1
9    }
10}
11
12print(puntajeDelEquipo)
13// Imprime "11"

En una instrucción if, el condicional debe ser una expresión de tipo booleano; esto significa, que una instrucción de la forma if puntaje { … } es un error, ya que no se compara implícitamente con cero.

Puedes usar if y let en conjunto para lidiar con valores que podrían no existir. Estos valores son representados como opcionales. Un valor opcional puede contener un valor o nil –para indicar la ausencia de un valor–. Agrega un signo de interrogación (?) después del tipo de un valor para señalar dicho valor como opcional.

1var cadenaOpcional: String? = "Hola"
2print(cadenaOpcional == nil)
3// Imprime "false"
4
5var nombreOpcional: String? = "John Appleseed"
6var saludo = "¡Hola!"
7
8if let nombre = nombreOpcional {
9    saludo = "Hola, \(nombre)."
10}

Experimenta

Cambia el valor de nombreOpcional a nil. ¿Cuál saludo obtienes? Agrega una cláusula else que defina un saludo diferente si nombreOpcional es nil.

Si el valor opcional es nil, el condicional evalúa a false y el código en las llaves es ignorado. En caso contrario, se obtiene el valor opcional y se le asigna a la constante después de let, lo cual hace que el valor obtenido esté disponible dentro del bloque de código.

Otra forma de lidiar con valores opcionales es proporcionar un valor por defecto a través del operador ??. Si el valor opcional no existe, se usa el valor por defecto en su lugar.

1let alias: String? = nil
2let nombreCompleto: String = "John Appleseed"
3let saludoInformal = "Hola, \(alias ?? nombreCompleto)"

Los bucles tipo switch soportan cualquier tipo de datos así como una gran variedad de operaciones comparativas, por lo que no se limitan a enteros y pruebas de calidad.

1let vegetal = "pimiento rojo"
2
3switch vegetal {
4case "apio":
5    print("Un par de frutos verdes más y tendrás un buen batido.")
6case "pepino", "cebolla":
7    print("Útiles para un buen sandwich.")
8case let x where x.hasSuffix("pimiento"):
9    print("¿Es un \(x) picante?")
10default:
11    print("Todo sabe bien en una sopa.")
12}
13// Imprime "¿Es un pimiento rojo picante?"

Experimenta

Remueve el caso default. ¿Cuál es el error que aparece?

Observa cómo es posible usar let con un patrón para asignar el valor que concuerde con dicho patrón a una constante.

Una vez ejecutado el código dentro del caso que concuerda, el programa abandona el bucle switch. La ejecución no continúa al siguiente caso, por lo que no es necesario indicar la salida del bucle explícitamente al final del código de cada caso.

Puedes usar for-in para iterar sobre los elementos de un diccionario al proporcionar un par de nombres que serán usados para cada par llave-valor. Los diccionarios son colecciones sin un orden particular, por lo que se itera sobre sus llaves y valores de manera arbitraria.

1let numerosInteresantes = [
2    "Primos": [2, 3, 5, 7, 11, 13],
3    "Fibonacci": [1, 1, 2, 3, 5, 8],
4    "Cuadrados": [1, 4, 9, 16, 25],
5]
6var mayor = 0
7
8for (_, numeros) in numerosInteresantes {
9    for numero in numeros {
10        if numero > mayor {
11            mayor = numero
12        }
13    }
14}
15
16print(mayor)
17// Imprime "25"

Experimenta

Reemplaza _ por una variable con nombre y haz seguimiento del tipo de número que resultó ser el mayor.

Usa while para repetir un bloque de código hasta que se satisfaga una condición. La condición del bucle puede ir al final, para asegurar que este se ejecute al menos una vez.

1var n = 2
2while n < 100 {
3    n *= 2
4}
5
6print(n) // Imprime "128"
7
8var m = 2
9repeat {
10    m *= 2
11} while m < 100
12
13print(m) // Imprime "128"

Es posible tener índices en un bucle al usar ..< para crear un rango de índices.

1var total = 0
2
3for i in 0..<4 {
4    total += i
5}
6
7print(total)
8// Imprime "6"

Usa ..< para crear un rango que omita el valor superior, y usa ... para crear uno que incluya ambos valores.

Funciones y Clausuras

Usa func para declarar una función. Para llamar una función, escribe una lista de argumentos en paréntesis después de su nombre. Usa -> para separar el nombre de los parámetros y su tipo del tipo que devuelve la función.

1func saludar(persona: String, dia: String) -> String {
2    return "Hola, \(persona), hoy es \(dia)."
3}
4
5saludar(persona: "Bob", dia: "Martes")

Experimenta

Remueve el parámetro dia. Agrega un parámetro que incluya el almuerzo especial de hoy en el saludo.

Por defecto, las funciones usan los nombres de sus parámetros como etiquetas para sus argumentos. Crea tu propia etiqueta para un argumento anteponiéndola al nombre del parámetro, o agrega _ para no usar una etiqueta para un argumento.

1func saludar(_ persona: String, en dia: String) -> String {
2    return "Hola, \(persona), hoy es \(dia)."
3}
4
5saludar("Juan", en: "Miercoles")

Usa una tupla para crear un valor compuesto; por ejemplo, para devolver múltiples valores desde una función. Los elementos de una tupla se pueden referenciar bien sea por nombre o por número.

1func calcularEstadisticas(puntajes: [Int]) -> (min: Int, max: Int, suma: Int) {
2    var min = puntajes[0]
3    var max = puntajes[0]
4    var suma = 0
5
6    for puntaje in puntajes {
7        if puntaje > max {
8            max = puntaje
9        } else if puntaje < min {
10            min = puntaje
11        }
12
13        suma += puntaje
14    }
15
16    return (min, max, suma)
17}
18
19let estadisticas = calcularEstadisticas(puntajes: [5, 3, 100, 3, 9])
20print(estadisticas.suma) // Imprime "120"
21print(estadisticas.2) // Imprime "120"

Las funciones pueden anidarse. Las funciones anidadas tienen acceso a variables que hayan sido declaradas en la función externa. Puedes usar funciones anidadas para organizar código que resulta extenso o complejo.

1func devolverQuince() -> Int {
2    var y = 10
3    func agregar() {
4        y += 5
5    }
6    agregar()
7
8    return y
9}
10devolverQuince()

Las funciones son un tipo de primera clase. Esto, quiere decir, que una función puede devolver otra función como su valor.

1func crearIncrementador() -> ((Int) -> Int) {
2    func agregarUno(numero: Int) -> Int {
3        return 1 + numero
4    }
5
6    return agregarUno
7}
8var incrementar = crearIncrementador()
9incrementar(7)

Una función puede tomar a otra función como uno de sus argumentos.

1func coincideAlguno(lista: [Int], condicion: (Int) -> Bool) -> Bool {
2    for elemento in lista {
3        if condicion(elemento) {
4            return true
5        }
6    }
7
8    return false
9}
10
11func menorQueDiez(numero: Int) -> Bool {
12    return numero < 10
13}
14
15var numeros = [20, 19, 7, 12]
16coincideAlguno(lista: numeros, condicion: menorQueDiez)

Las funciones son, en realidad, un caso especial de closure: bloques de código que pueden ser llamados más tarde. El código en una clausura tiene acceso a elementos como variables y funciones que están disponibles en el ámbito en el cual se creó la clausura, incluso cuando la clausura se encuentra en un ámbito diferente al momento de ejecutarse; ya has visto un ejemplo de esto con las funciones anidadas. Puedes crear una clausura anónima al encerrar el código en llaves ({}). Usa in para separar los argumentos y el tipo devuelto por la función del cuerpo de la función.

1numeros.map({ (numero: Int) -> Int in
2    let resultado = 3 * numero
3
4    return resultado
5})

Experimenta

Reescribe la clausura de manera que devuelva cero para todos los números impares.

Existen muchas maneras de crear una clausura de forma más concisa. Cuando se conoce el tipo de una clausura, como es el caso de un callback para un delegado, puedes omitir el tipo de sus parámetros, el tipo devuelto, o de ambos. Las clausuras de una sola sentencia devuelven el valor de su única sentencia de manera implícita.

1let mappedNumbers = numeros.map({ numero in 3 * numero })
2print(mappedNumbers)
3// Imprime "[60, 57, 21, 36]"

Puedes referenciar parámetros por número en vez de nombre; este enfoque es, especialmente, útil para clausuras muy concisas. Una clausura que se pasa como el último argumento de una función puede aparecer inmediatamente después de los paréntesis. Cuando una clausura es el único argumento de una función, puedes omitir los paréntesis por completo.

1let numerosOrdenados = numeros.sorted { $0 > $1 }
2print(numerosOrdenados)
3// Imprime "[20, 19, 12, 7]"

Objetos y Clases

Usa class seguido del nombre de la clase para crear una clase. Para declarar una propiedad de una clase, se escribe igual que al declarar una constante o variable, excepto que esta existiría en el contexto de una clase. Similarmente, la declaración de métodos y funciones se hace de la misma forma.

1class Figura {
2    var numeroDeLados = 0
3    func descripcionBasica() -> String {
4        return "Una figura con \(numeroDeLados) lados."
5    }
6}

Experimenta

Agrega una constante con let y otro método que tome un argumento.

Crea una instancia de una clase al poner paréntesis después del nombre de la clase. Usa sintaxis de punto para acceder a las propiedades y métodos de la instancia.

1var figura = Figura()
2figura.numeroDeLados = 7
3var descripcionFigura = descripcionBasica()

A esta versión de la clase Figura le hace falta algo importante: un inicializador que establezca la clase al crear una instancia. Usa init para crear uno.

1class FiguraConNombre {
2    var numeroDeLados: Int = 0
3    var nombre: String
4
5    init(nombre: String) {
6        self.nombre = nombre
7    }
8
9    func descripcionBasica() -> String {
10        return "Una figura con \(numeroDeLados) lados."
11    }
12}

Observa cómo se usa self para diferenciar la propiedad nombre del argumento (del inicializador) nombre. Al crear una instancia de una clase, los argumentos del inicializador se pasan como cuando se llama una función. Cada propiedad requiere que se le asigne un valor, bien sea al declararla (como con numeroDeLados) o en el inicializador (como con nombre).

Usa deinit para crear un «desinicializador» (del inglés deinitializer) si necesitas llevar a cabo alguna limpieza antes de que el objeto sea «desasignado» (del inglés deallocated).

Las subclases incluyen el nombre de su súperclase después de su propio nombre, separados por una coma. No es requerimiento de las clases pasar ninguna clase base estándar a las subclases, por lo que puedes incluir u omitir una súperclase si así lo requieres.

Aquellos métodos de una subclase que sobreescriben la implementación de una súperclase, se marcan con override; sobreescribir un método por accidente, sin usar override, es detectado por el compilador como un error. El compilador también detecta métodos con override que no sobreescriben ningún método de la súperclase.

1class Cuadrado: FiguraConNombre {
2    var longitudLado: Double
3
4    init(longitudLado: Double, nombre: String) {
5        self.longitudLado = longitudLado
6        super.init(nombre: nombre)
7        numeroDeLados = 4
8    }
9
10    func area() -> Double {
11        return longitudLado * longitudLado
12    }
13
14    override func descripcionBasica() -> String {
15        return "Un cuadrado con lados de longitud \(longitudLado)."
16    }
17}
18
19let prueba = Cuadrado(longitudLado: 5.2, nombre: "cuadrado de prueba")
20prueba.area()
21prueba.descripcionBasica()

Experimenta

Crea otra subclase de FiguraConNombre llamada Circulo, que tome un radio y un nombre como argumentos de su inicializador. Implementa los métodos area() y descripcionBasica() en la clase Circulo.

Aparte de simples propiedades que se almacenan, las propiedades pueden tener un getter y un setter.

1class TrianguloEquilatero: FiguraConNombre {
2    var longitudLado: Double = 0.0
3
4    init(longitudLado: Double, nombre: String) {
5        self.longitudLado = longitudLado
6        super.init(nombre: nombre)
7        numeroDeLados = 3
8    }
9
10    var perimetro: Double {
11        get {
12            return 3.0 * longitudLado
13        }
14
15        set {
16            longitudLado = newValue / 3.0
17        }
18    }
19
20    override func descripcionBasica() -> String {
21        return "Un triángulo equilátero con lados de longitud \(longitudLado)."
22    }
23}
24
25var triangulo = TrianguloEquilatero(longitudLado: 3.1, nombre: "un triángulo")
26print(triangulo.perimetro) // Imprime "9.3"
27triangulo.perimetro = 9.9
28print(triangulo.longitudLado) // Imprime "3.3000000000000003"

En el setter de perimetro, el nuevo valor tiene el nombre implícito de newValue. Puedes proporcionar un nombre explícito en paréntesis después de set.

Observa cómo el inicializador de la clase TrianguloEquilatero tiene tres pasos diferentes:

  1. Establecer el valor de las propiedades declaradas por la subclase.
  2. Llamar al inicializador de la súperclase.
  3. Cambiar el valor de las propiedades definidas por la súperclase. Cualquier otra configuración adicional que use métodos, getters, o setters también puede llevarse a cabo en este punto.

Si no necesitas calcular la propiedad, pero igual necesitas proporcionar código que se ejecuta antes y después de establecer un nuevo valor, usa willSet and didSet. El código que proporciones se ejecutará cada vez que el valor cambie fuera del inicializador. Por ejemplo, la clase a continuación se asegura de que la longitud de los lados de su triángulo siempre sea la misma que la longitud de los lados de su cuadrado.

1class TrianguloYCuadrado {
2    var triangulo: TrianguloEquilatero {
3        willSet {
4            cuadrado.longitudLado = newValue.longitudLado
5        }
6    }
7
8    var cuadrado: Cuadrado {
9        willSet {
10            triangulo.longitudLado = newValue.longitudLado
11        }
12    }
13
14    init(tamano: Double, nombre: String) {
15        cuadrado = Cuadrado(longitudLado: tamano, nombre: nombre)
16        triangulo = TrianguloEquilatero(longitudLado: tamano, nombre: nombre)
17    }
18}
19
20var trianguloYCuadrado = TrianguloYCuadrado(size: 10, nombre: "otra figura de prueba")
21print(trianguloYCuadrado.cuadrado.longitudLado)
22// Imprime "10.0"
23print(trianguloYCuadrado.triangulo.longitudLado)
24// Imprime "10.0"
25trianguloYCuadrado.cuadrado = Cuadrado(longitudLado: 50, nombre: "cuadrado más grande")
26print(trianguloYCuadrado.triangulo.longitudLado)
27// Imprime "50.0"

Al trabajar con valores opcionales, puedes escribir ? antes de operaciones como métodos, propiedades, y subscripting. Si el valor antes de ? es nil, todo lo que sigue a ? es ignorado y el valor de toda la expresión es nil. En caso contrario, se obtiene el valor opcional y todo lo que sigue a ? opera sobre el valor obtenido. En ambos casos, el valor de toda la expresión es un valor opcional.

1let cuadradoOpcional: Cuadrado? = Cuadrado(longitudLado: 2.5, nombre: "cuadrado opcional")
2let longitudLado = cuadradoOpcional?.longitudLado

Enumeraciones y Estructuras

Utiliza enum para crear una enumeración. Al igual que las clases y todos los demás tipos con nombre, las enumeraciones pueden tener métodos asociados con ellas.

1enum Escala: Int {
2    case ace = 1
3    case dos, tres, cuatro, cinco, seis, siete, ocho, nueve, diez
4    case jack, reina, rey
5
6    func descripcionBasica() -> String {
7        switch self {
8        case .ace:
9            return "ace"
10        case .jack:
11            return "jack"
12        case .reina:
13            return "reina"
14        case .rey:
15            return "rey"
16        default:
17            return String(self.rawValue)
18        }
19    }
20}
21
22let ace = Escala.ace
23let valorBrutoAce = ace.rawValue

Experimenta

Crea una función que compare dos valores de tipo Escala comparando sus valores brutos.

De manera predeterminada, Swift asigna los valores brutos comenzando por cero y aumentando en uno cada vez, pero es posible cambiar dicho comportamiento al especificar valores explícitamente. En el ejemplo anterior, a Ace se le asigna explícitamente un valor bruto de 1 y el resto de los valores brutos se asignan en orden. También es posible utilizar cadenas de texto o números de coma flotante como el tipo bruto de una enumeración. Usa la propiedad rawValue para acceder al valor bruto del caso de una enumeración.

Utiliza el inicializador init?(RawValue:) para crear una instancia de una enumeración a partir de un valor bruto. Este inicializador devuelve el caso (de la enumeración) que coincida con el valor bruto o nil si no existe tal enumeración Escala.

1if let escalaTransformada = Escala(rawValue: 3) {
2    let descripcionTres = escalaTransformada.descripcionSimple()
3}

Los valores de los casos de una enumeración son valores reales, no solo otra forma de escribir sus valores brutos. De hecho, en los casos en los que no existe un valor bruto significativo, no es necesario que proporciones uno.

1enum Palo {
2    case corazones, diamantes, picas, treboles
3
4    func descripcionBasica() -> String {
5        switch self {
6        case .corazones:
7            return "corazones"
8        case .diamantes:
9            return "diamantes"
10        case .picas:
11            return "picas"
12        case .treboles:
13            return "treboles"
14        }
15    }
16}
17
18let corazones = Palo.corazones
19let descripcionCorazones = corazones.descripcionBasica()

Experimenta

Crea un método color() para la enumeración Palo que devuelva «negro» para picas y tréboles, y «rojo» para corazones y diamantes.

Fíjate en las dos formas en que se referenció el caso corazones de la enumeración: al asignar un valor a la constante corazones, el caso Palo.corazones se referencia por su nombre completo porque la constante no tiene un tipo especificado explícitamente. Dentro del bucle switch, el caso se referencia mediante la forma abreviada .corazones porque ya sabemos que el valor de self es un tipo de palo. Puedes utilizar la forma abreviada siempre que el tipo del valor ya sea conocido.

Si una enumeración tiene valores brutos, se establece que dicho valores forman parte de la declaración, lo que significa que cada instancia de un caso de enumeración en particular siempre tendrá el mismo valor bruto. Otra opción para los casos de enumeración es tener valores asociados con el caso; estos valores se determinan al crear la instancia y pueden ser diferentes para cada instancia de un caso de enumeración. Puedes ver los valores asociados como si actuaran como propiedades almacenadas de la instancia del caso de enumeración. Por ejemplo, considera el caso en el que se le pide a un servidor las horas de salida y puesta del sol. El servidor responderá con la información solicitada o responderá con una descripción de lo que haya salido mal.

1enum RespuestaServidor {
2    case resultado(String, String)
3    case falla(String)
4}
5
6let success = RespuestaServidor.resultado("6:00 am", "6:00 pm")
7let falla = RespuestaServidor.falla("Sin respuesta.")
8
9switch success {
10case let .resultado(amanecer, atardecer):
11    print("El amanecer se da a las \(amanecer) y el atardecer se da a las \(atardecer).")
12case let .falla(mensaje):
13    print("Error...  \(mensaje)")
14}

Experimenta

Agrega un tercer caso a RespuestaServidor y al bucle switch.

Observa cómo las horas de amanecer y atardecer se extraen del valor RespuestaServidor como parte de la comparación del valor con los casos del bucle switch.

Usa struct para crear una estructura. Las estructuras soportan muchos de los mismos comportamientos que las clases, incluyendo métodos e inicializadores. Una de las diferencias más importantes entre estructuras y clases es que las estructuras siempre son copiadas al pasarlas en tu código, pero las clases se pasan por referencia.

1struct Carta {
2    var escala: Escala
3    var palo: Palo
4    func descripcionSimple() -> String {
5        return "El \(escala.descripcionBasica()) de \(palo.descripcionBasica())."
6    }
7}
8
9let tresDePicas = Carta(escala: .tres, palo: .picas)
10let descripcionTresDePicas = tresDePicas.descripcionBasica()

Experimenta

Crea una función que devuelva un array que contenga una baraja completa de cartas, con una carta de cada combinación de escala y palo.

Protocolos y Extensiones

Usa protocol para declarar un protocolo.

1protocol ProtocoloEjemplo {
2    var descripcionBasica: String { get }
3    mutating func ajustar()
4}

Las classes, enumeraciones, y estructuras pueden adoptar protocolos.

1class ClaseBasica: ProtocoloEjemplo {
2     var descripcionBasica: String = "Una clase muy básica."
3     var otraPropiedad: Int = 69105
4     func ajustar() {
5          descripcionBasica += " Ahora 100% ajustada."
6     }
7}
8
9var a = ClaseBasica()
10a.ajustar()
11let descripcionA = a.descripcionBasica
12
13struct EstructuraBasica: ProtocoloEjemplo {
14     var descripcionBasica: String = "Una estructura básica"
15     mutating func ajustar() {
16          descripcionBasica += " (ajustada)"
17     }
18}
19
20var b = EstructuraBasica()
21b.ajustar()
22let descripcionB = b.descripcionBasica

Experimenta

Agrega otro requisito a ProtocoloEjemplo. ¿Qué cambios debes hacer en ClaseBasica y EstructuraBasica para que estas tengan conformidad con el protocolo?

Nota el uso de la palabra clave mutating en la declaración de EstructuraBasica para marcar un método que modifica la estructura. En la declaración de ClaseBasica no se necesita que ninguno de sus métodos esté marcado como modificador ya que los métodos de una clase siempre pueden modificar la clase misma.

Utiliza extension para agregar funcionalidad a un tipo existente, como nuevos métodos y propiedades calculadas. Puedes usar una extensión para hacer que un tipo se ajuste a un protocolo, bien sea un tipo que se declara en otro lugar, o incluso un tipo que se importa de una biblioteca o framework.

1extension Int: ProtocoloEjemplo {
2    var descripcionBasica: String {
3        return "El número \(self)"
4    }
5    mutating func ajustar() {
6        self += 42
7    }
8 }
9print(7.descripcionBasica)
10// Imprime "El número 7"

Experimenta

Crea una extensión para el tipo Double que agregue la propiedad valorAbsoluto.

Puedes usar el nombre de un protocolo como cualquier otro tipo con nombre; por ejemplo, para crear una colección de objetos que tiene tipos diferentes, pero que todos se ajusten a un solo protocolo. Al trabajar con valores cuyo tipo es un tipo de protocolo, los métodos externos a la definición del protocolo no estarán disponibles.

1let valorProtocolo: ProtocoloEjemplo = a
2print(valorProtocolo.descripcionBasica)
3// Imprime "Una clase muy básica. Ahora 100% ajustada."
4// print(valorProtocolo.otraPropiedad)  // Remueve este comentario para ver el error

Aun cuando el tipo de runtime de la variable valorProtocolo es del tipo ClaseBasica, el compilador lo trata como el tipo dado de ProtocoloEjemplo. Esto significa que no puedes acceder, de manera accidental, a métodos o propiedades que la clase implementa además de su conformidad con el protocolo.

Manejo de Errores

Puedes representar errores mediante cualquier tipo que adopte el protocolo Error.

1enum ErrorImpresora: Error {
2    case noHayPapel
3    case noHayToner
4    case enLlamas
5}

Usa throw para arrojar un error y throws para marcar una función que puede arrojar un error. Si arrojas un error en una función, la función se interrumpe inmediatamente y el código que llamó a la función se encargará de manejar el error.

1func enviar(tarea: Int, impresoraDestino nombreImpresora: String) throws -> String {
2    if nombreImpresora == "Nunca Tiene Toner" {
3        throw ErrorImpresora.noHayToner
4    }
5
6    return "Tarea enviada"
7}

Hay muchas maneras de manejar los errores. Una forma es usando do-catch. Dentro del bloque do, marcas el código que puede arrojar un error escribiendo try delante de él. Dentro del bloque catch, al error se le asigna automáticamente el nombre error, a menos que le asignes un nombre diferente.

1do {
2    let respuestaImpresora = try enviar(tarea: 1040, impresoraDestino: "Bi Sheng")
3    print(respuestaImpresora)
4} catch {
5    print(error)
6}
7// Imprime "Tarea enviada"

Experimenta

Cambia el nombre de la impresora a "Nunca Tiene Toner", de manera que la función enviar(tarea:impresoraDestino:) arroje un error.

Puedes usar múltiples bloques catch que manejen errores específicos. Para esto, define un patrón después de catch al igual que lo haces después de case en un bucle switch.

1do {
2    let respuestaImpresora = try enviar(tarea: 1440, impresoraDestino: "Gutenberg")
3    print(respuestaImpresora)
4} catch ErrorImpresora.enLlamas {
5    print("Dejaré esto por aquí, con el resto del fuego.")
6} catch let errorImpresora as ErrorImpresora {
7    print("Error impresora: (errorImpresora).")
8} catch {
9    print(error)
10}
11// Imprime "Tarea enviada"

Experimenta

Agrega código de manera que se arroje un error dentro del bloque do. ¿Qué tipo de error es necesario arrojar para que este sea manejado por el primer bloque catch? ¿Qué tal con el segundo y tercer bloque?

Otra forma de manejar los errores es usando try? para convertir el resultado en un opcional. Si la función arroja un error, el error específico se descarta y el resultado es nil. De lo contrario, el resultado es un opcional que contiene el valor que devolvió la función.

1let exitoImpresora = try? enviar(tarea: 1884, impresoraDestino: "Mergenthaler")
2let fallaImpresora = try? enviar(tarea: 1885, impresoraDestino: "Nunca Tiene Toner")

Usa defer para crear un bloque de código que se ejecute después de todo el código de una función, justo antes de que la función devuelva su valor. El código se ejecuta sin importar que la función arroje un error. Puedes utilizar defer para escribir códigos de configuración y limpieza, uno al lado del otro, aun cuando estos deban ejecutarse en momentos diferentes.

1var refrigeradorEstaAbierto = false
2let contenidoRefrigerador = ["leche", "huevos", "sobras"]
3
4func conocerContenidoRefrigerador(_ alimento: String) -> Bool {
5    refrigeradorEstaAbierto = true
6
7    defer {
8        refrigeradorEstaAbierto = false
9    }
10
11    let resultado = contenidoRefrigerador.contains(alimento)
12
13    return resultado
14}
15
16conocerContenidoRefrigerador("banana")
17print(refrigeradorEstaAbierto)
18// Imprime "false"

Genéricos

Escribe un nombre entre paréntesis angulares (<>) para crear una función o tipo genérico.

1func crearArray<Item>(repitiendo item: Item, numeroDeVeces: Int) -> [Item] {
2    var resultado: [Item] = []
3
4    for _ in 0..<numeroDeVeces {
5        resultado.append(item)
6    }
7
8    return resultado
9}
10
11crearArray(repitiendo: "toc", numeroDeVeces: 4)

También puedes crear formas genéricas de funciones y métodos, al igual que, de clases, enumeraciones, y estructuras.

1// Reimplementamos el tipo opcional de la librería estándar de Swift
2enum OptionalValue<Wrapped> {
3    case none
4    case some(Wrapped)
5}
6var posibleEntero: OptionalValue<Int> = .none
7posibleEntero = .some(100)

Utiliza where justo antes del cuerpo de la función para especificar una lista de requerimientos; por ejemplo, para requerir el tipo para implementar un protocolo, para requerir que dos tipos sean el mismo, o para requerir que una clase tenga una súperclase en particular.

1func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
2    where T.Element: Equatable, T.Element == U.Element
3{
4    for lhsItem in lhs {
5        for rhsItem in rhs {
6            if lhsItem == rhsItem {
7                return true
8            }
9        }
10    }
11
12    return false
13}
14anyCommonElements([1, 2, 3], [3])

Experimenta

Modifica la función anyCommonElements(_:_:) para crear una función que devuelva un array de los elementos que dos secuencias cualesquiera tienen en común.

Escribir <T: Equatable> es igual a escribir <T> ... where T: Equatable.