CLOSURES en Swift en Español - Curso Swift | Tutorial Swift

Aprende Swift desde cero

Los closures son bloques de funcionalidad, son muy similares a los bloques en C y Objective-C y a las lamdas en otros lenguajes de programación.

Los closures pueden capturar y almacenar referencias de cualquier constante o variable del contexto en el cual fueron definidas.

Podemos decir, que los closures tiene un estilo limpio y claro, con optimizaciones que fomentan una sintaxis breve y ordenada en escenarios comunes:

  • Podemos inferir parámetros y retornar tipos de valores de su contexto.
  • Si los closures son de una línea, podemos retornar un valor sin poner return.
  • Podemos usar $0, $1, etc dentro de los closures para simplificar los argumerntos.
  • El contenido de los closures va dentro de llaves, igual que en las funciones, { }

Ejemplo con sorted(by:)

La libreria standard de Swift proporciona un método llamado sorted(by:), el cual ordena una array.

El método sorted(by:) acepta un closure que espera dos argumentos del mismo tipo y retorna un booleano. El booleano será true si el primer valor debe aparecer antes que el segundo valor, y será false en caso contrario.

Vamos a ver un ejemplo ordenando un array de strings que puedes escribir de distintas maneras:

var names = ["Chris", "Alex", "SwiftBeta", "iOS", "Apple"]

names.sorted { (firstValue, secondValue) -> Bool in
    return firstValue < secondValue
}

names.sorted { (firstValue, secondValue) -> Bool in
    firstValue < secondValue
}

names.sorted { $0 < $1 }

names.sorted(by: <)

// El resultado es una lista ordenada alfabéticamente de A-Z
// ["Alex", "Apple", "Chris", "iOS", "SwiftBeta"]
Ejemplo de Closure en Swift

El último caso nos da el mismo resultado que el primero con muchas menos líneas de código, la última línea es mucho más limpia, legible y clara.

En lugar de definir la lógica dentro de las llaves, lo puedes hacer de otra manera. Puedes crear una función que acepte el mismo tipo que sorted(by:) es decir, para seguir en la misma línea del ejemplo anterior podemos crear una función con el  tipo (String, String) -> Bool y pasársela a sorted(by:) vamos a ver como lo haríamos:

func backward(_ stringOne: String, _ stringTwo: String) -> Bool {
    return stringOne > stringTwo
}

var reversedNames = names.sorted(by: backward)

// El resultado es una lista ordenada alfabéticamente de Z-A
// ["SwiftBeta", "iOS", "Chris", "Apple", "Alex"]
Pasar funciones como closures en Swift

En lugar de tener un closure que abre las llaves en sorted(by:) le hemos pasado una función del mismo tipo que espera sorted(by:).

Sintaxis de los closures en Swift

Los closures tienen la siguiente sintaxis:

Los parámetros son como en las funciones, podemos pasarle parámetros sin problema al closure, hay algunas restricciones como no aceptar valores por defecto.

reversedNames = names.sorted(by: { (stringOne: String, stringTwo: String) -> Bool in
    return stringOne < stringTwo
})

El ejemplo anterior es exactamente lo mismo que hemos hecho unos ejemplos más arriba, cuando hemos pasado la función backward a sorted(by:). El resultado es exactamente el mismo.

Inferir tipos, return implícitos y argumentos $0

Muchas veces el valor se podrá inferir, es decir, al tener un array de strings, al hacer:

names.sorted { (firstValue: String, secondValue: String) -> Bool in
    return firstValue < secondValue
}

podemos no especificar los tipos del parámetro

names.sorted { (firstValue, secondValue) -> Bool in
    return firstValue < secondValue
}

otro detalle importante es que cuando tenemos una única línea de nuestro closure y queremos devolver un valor, no hace falta poner el return. Es decir, podemos hacer lo siguiente:

names.sorted { (firstValue, secondValue) -> Bool in
    firstValue < secondValue
}

por último comentarte que podemos omitir el nombre del parámetro y user $0, $1, $2, etc. El número 0 indica si es el primer parámetro, el 1 si es el segundo, etc.

names.sorted { $0 < $1 }
  • $0 representaría el parámetro de firstValue
  • $1 represnetaría el parámetro secondValue
  • etc

Trailing Closures en Swift

Cuando necesitamos pasar un closure a una función y es el último parámetro se transforma es un trailing closure.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    
}

// Así es como llamamos a la función

someFunctionThatTakesAClosure {
    
}
Trailing Closure en Swift

Los trailing closures son muy útiles cuando el closure tiene muchas líneas de código y no se puede representar en solo una. Un ejemplo es cuando usamos el método map(_:) que toma como único parámetro un closure.

map(_:) es realmente útil ya que podemos transformar lo que hay dentro del array y devolver un nuevo array con otro tipo distinto.

var numbersString = ["1", "2", "3", "4", "5"]
var numbersInt = numbersString.map { Int($0) }
numbersInt

// El resultado es un array de Int [1, 2, 3, 4, 5]

También podríamos haber creado una función con el mapeo y pasársela a map(_:).

func mapStringToInt(_ value: String) -> Int {
    return Int(value)!
}

numbersString.map(mapStringToInt)

El resultado es idéntico en los dos ejemplos.

Múltiples Closures en una función

Ahora imagínate que tienes más de un closure en una función. Quieres saber si una acción ha ido bien o ha ido mal, para eso "crearemos dos caminos distintos" dos closures para cada camino.

Creamos la función que acepta un string y los dos trailing closure:

enum BackendError {
    case customError
}

func getDataFromBackend(status: String,
                        onSuccess: () -> Void,
                        onFailure:(BackendError) -> Void) {
    if status == "OK" {
        onSuccess()
    } else {
        onFailure(.customError)
    }
}

y ahora vamos a llamar a la función con status "OK", por lo tanto irá por el camino de que todo ha ido bien:

getDataFromBackend(status: "OK", onSuccess: {
    print("Todo OK")
}) { (error) in
    print("Error \(error)")
}

// El resultado que aparece por pantalla es "Todo OK"

Vamos hacer lo mismo, pero forzando a que sea un error, vamos a llamar a la función con status "qq":

getDataFromBackend(status: "OK", onSuccess: {
    print("Todo OK")
}) { (error) in
    print("Error \(error)")
}

// El resultado que aparece por pantalla es "Error customError"

Capturando Valores

Un closure puede capturar constantes y variables del contexto en que son definidas. Por lo tanto el closure puede modificar valores de aquella constantes y variables que están en su body, incluso si el scope original que ha definido las constantes y variables ha dejado de existir.

En Swift la manera más simple de capturar valores es con el anidado de funciones. Una función anidada puede capturar cualquier variable o constante de su función padre.

Vamos a mostrar un ejemplo con una función que aparece en la documentación de Swift:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

Es una función y dentro de ella hay otra función, es decir, solo tenemos una función anidada que captura dos valores, runningTotal y amount. Al capturar estas variables por referencia, nos aseguramos que no desapareceran cuando makeIncrementer finalice.

El tipo de retorno de makeIncrementer es () -> Int. Esto significa que retorna una función en lugar de un simple valor. La función no acepta ningún parámetro y retorna un Int cada vez que se llama.

Vamos a probar nuestro incrementador, crearemos dos, un incrementador por 10 y un incrementador por 7

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
incrementByTen()

// Resultado de incrementByTen es 20

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// Resultado de incrementBySeven es 7

incrementByTen()
// Resultado de incrementByTen es 30

Al crear el segundo incrementador, runningTotal tiene su propia referencia en su incrementador y por lo tanto no afecta al primer incrementador.


Los closure son Reference Types

Las funciones y los closures son reference types. Cuando asignas una función o un closure a una variable o constante, en realidad estás creando una referencia. Es por eso que podemos hacer lo siguiente:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// Resultado de incrementByTen es 40

incrementByTen()
// Resultado de incrementByTen es 50

Ejemplo del Reference Type de los Closures

@escaping en Swift (Escaping Closures)

Un closure es escaping cuando el closure es pasado como argumento a la función, pero éste es llamado después de que la función haya acabado.

Un ejemplo muy claro es cuando hacemos una llamada a backend y esperamos la respuesta, el closure que nos indicará que se ha completado debe ser @escaping ya que la función acabará antes de llamar al closure (es una llamada asíncrona). Vamos a simularlo en un ejemplo:

func getDataFromBackend(completionHandler: () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        completionHandler()
    }
}

// Error: Escaping closure captures non-escaping parameter 'completionHandler'
Escaping Closure en Swift

En el snippet de código anterior tenemos un error, ya que necesitamos que completionHandler sea @escaping, ya que la función acaba antes de llamar al completionHandler, debemos indicar que queremos llamarlo más tarde, para ello modificamos el código anterior:

func getDataFromBackend(completionHandler: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        completionHandler()
    }
}

Ahora funcionaría perfectamente. No tenemos ningún mensaje de error ya que estamos diciendo que completionHandler será llamado más tarde.


Autoclosures en Swift

Los autoclosures nos permiten llamar al código cuando lo vayamos a ejecutar. Postergamos llamar a la función para cuando sea realmente necesario. Puede que no tenga sentido gastar computacionalmente recursos. Vamos a ver unos ejemplos.

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// El resultado es 5

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// El resultadoc continua siendo 5
Autoclosures en Swift

Al hacer el segundo print sigue mostrando que hay 5 elementos en el array customersInLine. Esto es debido a que no se ha llamado a customerProvider(), se ha postergado su ejecución para cuando queramos. Vamos a ver ahora que si llamamos a la función el valor del array se decrementa:

print("Now serving \(customerProvider())!")

print(customersInLine.count)
// Ahora el resultado del array es 4

Al llamar a la función dentro del print el valor se decrementa. Hemos postergado su ejecución.

Podemos hacer lo mismo cuando pasamos una función como parámetro a otra función.

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}

serve(customer: { customersInLine.remove(at: 0) } )
// Ahora el resultado es "Now serving Alex!"
Función en Swift

En este ejemplo estamos pasando una función que acepta una función de tipo () -> String, que es lo mismo que hemos pasado como parámetro.

{ customersInline.remove(at:0) }

Y dentro de serve hemos ejecutado esta función en el punto que hemos querido, en este caso dentro del print. Podemos omitir la sintaxis de añadir llaves al insertar esta función como parámetro de otra función, para eso podemos usar @autoclosure de la siguiente manera:

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}

serve(customer: customersInLine.remove(at: 0) )
// Ahora el resultado es "Now serving Ewa!"
Autoclosures en Swift