Blog de Desarrollo en Swift para Plataformas Apple y Linux

Swift – ¿Qué es un Closure?

Get real time updates directly on you device, subscribe now.

En este Tutorial Swift aprenderemos sobre los closures, bloques autónomos funcionales que podemos utilizar en nuestro código. En Swift estos serían como las funciones anónimas de toda la vida, similares a los bloques en C o Objective-C y a los lambdas en otros lenguajes de programación, por lo que no nos debe de resultar muy difícil su dominio, de hecho ya los hemos usado, ya que las funciones son consideradas un tipos especial de closures.

Las expresiones closures en Swift se diferencian de las funciones en que no tienen nombre y en que podemos crear bloques de código de una manera bastante simple sin tener que generar una declaración completa como en el caso de las funciones donde todo esto si es más estricto, esta flexibilidad nos permite, de una manera mucho más fluida, pasar los closures como parámetro o como tipo de retorno.

Sintaxis

Los closures tienen un estilo muy bien definido, optimizaciones que fomentan limpieza y claridad con un enfoque en la legibilidad en cualquiera de los escenarios donde estas se puedan encontrar. La sintaxis básica de un closure no es para nada compleja ni enrevesada, veamos un ejemplo de la forma general que adoptan:

{ (parámetros) -> tipo_de_retorno in declaraciones }

…como ven apartando las llaves y la ausencia de un nombre el resto es bastante parecido a la definición de una función.

El método sort

Imaginemos que somos los organizadores de una comunidad que cuenta con varias organizaciones y queremos mantener un registro de cuantos voluntarios tenemos por cada organización por lo que hemos creado un arreglo donde almacenamos toda esta información:

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

…hemos introducido la cantidad de voluntarios en la medida que nos han dado la información, por lo que el arreglo lo tenemos completamente desorganizado, sería genial que lo pudiéramos tener de menor a mayor. Aquí llegan las buenas noticias, sí pues la librería estándar de Swift nos ofrece el método sort(_:) que nos permite especificar como organizaremos nuestro Array.

El método sort(_:) toma un argumento: un closure que describe como se debe de organizar el Array. El closure toma dos argumentos cuyos tipos tienen que ser iguales al tipo de dato de cada elemento en el arreglo y debe de retornar un valor booleano.

Estos dos argumentos son comparados para generar el valor de retorno, lo que representa si la instancia en el primer argumento debe de ser organizada ante de la instancia del segundo argumento.

Para esto nos valemos del operador < (menor que) en pos de lograr un orden ascendente o descendente en caso de usar > (mayor que). El método sort(_:) termina por retornar un nuevo Array pero ya organizado tal  y como hayamos especificado en el closure.

Veamos un ejemplo:

//: Playground - noun: a place where people can play

import UIKit

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

func sortAscending(firtElement: Int, secondElement: Int) -> Bool {

    return firtElement < secondElement

} // sortAscending

let volunteerSorted = volunteerCounts.sort(sortAscending)

En este ejemplo hemos creado una función llamada sortAscending la cual toma dos argumentos, en este caso dos enteros del array y analiza cual es menor que el otro.

En caso del primer elemento ser menor devuelve True como valor booleano de retorno y con esto le informa al método sort(_:) que este elemento debe ir primero que el segundo en el nuevo array que se está conformando y que quedaría de la siguiente forma:

[1, 2, 3, 13, 32, 40, 53, 77]

En caso de que se lo estén preguntando, hemos usado la función sortAscending ya que todas las funciones son closures con nombres, de hecho como ven en la llamada, los parámetros son omitidos por nosotros y dejamos esta tarea el método sort(_:) que es el encargado de esta tarea.

Todo esto es posible ya que se le está dando un trato de closure a esta función, de lo contrario y en otro contexto el compilador nos obligaría a que especificar los argumentos.

Ahora, en lugar de declarar una función aparte hagamos uso de una expresión closure y optimicemos el código del ejemplo anterior:

//: Playground - noun: a place where people can play

import UIKit

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

let volunteerSorted = volunteerCounts.sort({

    (firtElement: Int, secondElement: Int) -> Bool in

    return firtElement < secondElement

    })

Esta refactorización nos deja con un código final mucho más limpio y elegante pero aún así tenemos demasiadas líneas de código, aquí una nueva versión:

//: Playground - noun: a place where people can play

import UIKit

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

let volunteerSorted = volunteerCounts.sort({ firtElement, secondElement -> Bool in firtElement < secondElement })
Como pueden observar hemos reducido nuestra expresión a una sola línea, pero aún podemos ir un poco más allá:
//: Playground - noun: a place where people can play

import UIKit

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

let volunteerSorted = volunteerCounts.sort({ $0 < $1 })

Aquí no acaba todo, esta versión del código puede sufrir aún más cambios y es que como el método sort(_:) solamente recibe un parámetro, un closure  en este caso, pues el compilador de Swift, que es todo un maestro infiriendo tipos y sintaxis, también nos permite hacer lo siguiente:

//: Playground - noun: a place where people can play

import UIKit

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

let volunteerSorted = volunteerCounts.sort { $0 < $1 }

…si este cambio no te parece mucho pues aquí tienes la última versión más simplificada:

//: Playground - noun: a place where people can play

import UIKit

var volunteerCounts = [1, 3, 40, 32, 2, 53, 77, 13]

let volunteerSorted = volunteerCounts.sort( < )

Como vemos es una versión mucho más compacta que el resto, pero también más críptica y puede que para personas con poca experiencia pueda incluso hasta lucir ilegible.

La versión que implementemos ya depende de nosotros, Swift como lenguaje nos permite ser tan flexible como estos ejemplos demuestran y creo que resulta suficiente para cualquiera que sea nuestra necesidad.

El método map

Ahora veamos un ejemplo similar, pero donde haremos uso del método map(_:) también miembro del tipo de dato Array y con el cual tenemos que interactuar haciendo uso de los closures. Analicemos su comportamiento:

let ArrayOfNumbers = [10, 20, 30, 40, 50]

let AddOneToEveryNumber = ArrayOfNumbers.map({

    (var number) -> Int in

        number++

        return number

}) // AddOneToEveryNumber - Closure

print(AddOneToEveryNumber)

…otra versión de este código pudiera ser:

let ArrayOfNumbers = [10, 20, 30, 40, 50]

let AddOneToEveryNumber = ArrayOfNumbers.map { (var number) -> Int in number++ }

print(AddOneToEveryNumber)

…la salida en pantalla de ambos códigos sería la misma:

[11, 21, 31, 41, 51]

Como podemos constatar en este ejemplo, el método map(_:) aplica el closure que hemos implementado a cada elemento en el Array, pero el método map también nos permite retornar un valor de retorno distinto al tipo de dato del parámetro de entrada.

Si presionamos Control + Espacio mientras el cursor se encuentra en la palabra map obtendremos una ayuda rápida que nos informa que parámetros recibe este método y que tipo de dato retorna, en este caso la ayuda nos informa que:

[T] map(transform: (Int) throws -> T) rethrows

…aquí podemos ver como al inicio se nos informa que el método devolverá un arreglo de T tipo de dato y que recibe como parámetro un closure / función que toma como argumento un entero y devuelve un valor de tipo T y T evidentemente puede ser Int, String Double, etc.

Las palabras claves throws y rethrows las veremos en próximas entregas de este Curso Swift así que por ahora solamente ignórenlas.

Veamos a continuación el código anterior con unas pocas modificaciones:

let digitNames = [

    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"

]

let ArrayOfNumbers = [10, 20, 30, 40, 50]

print("Arreglo inicial: \(ArrayOfNumbers)")

let ConvertToTheStringEquivalent = ArrayOfNumbers.map {

    (var number) -> String in

    var output = ""

    while number > 0 {

        output = digitNames[number % 10]! + output

        number /= 10

    } // while

    return output

} // ConvertToTheStringEquivalent - Closure

print("Arreglo final: \(ConvertToTheStringEquivalent)")

…la salida en pantalla de este código es:

Arreglo inicial: [10, 20, 30, 40, 50]
Arreglo final: ["OneZero", "TwoZero", "ThreeZero", "FourZero", "FiveZero"]

…en este ejemplo el funcionamiento es similar a cada elemento del arreglo se le aplica el closure implementado y se retorna al final un nuevo arreglo pero esta vez de tipo String con el equivalente en texto de cada dígito como podemos observar en la salida en pantalla.

Capturando valores

Un closure puede capturar tanto constantes como variables del entorno que lo rodea y, con capturar, me refiero a que puede acceder a estos y modificar sus valores, incluso cuando el ámbito de estos ya no existe. La forma más simple en la que podemos encontrar un caso así es cuando no encontramos ante funciones anidadas, veamos un ejemplo:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

    var runningTotal = 0

    func incrementer() -> Int {

        runningTotal += amount

        return runningTotal

    } // incrementer

    return incrementer

} // makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())
print(incrementByTen())
print(incrementByTen())

…en este ejemplo la función makeIncrementer toma como parámetro un valor Int y retorna una función sin argumentos de entrada y que retorna un valor Int. Hasta aquí todo bien, ahora comienza lo interesante y es en la línea 3 donde definimos la variable runningTotal la cual almacena el valor total que vamos usando como base cada vez que hacemos una llamada a la función que retornamos.

La función anidada incrementer como ya comentamos al inicio no toma argumento y devuelve un valor Int, dentro del cuerpo de esta sumamos al valor de runningTotal el incremento especificado cuando se efectuó la llamada a la función makeIncrementer para finalizar retornando este valor en la línea 9, la línea 13 retorna la función anidada acorde al valor de retorno especificado por makeIncrementer.

En la línea 17 declaramos la constante llamada incrementByTen la que igualamos al valor retornado por la función makeIncrementer pasándole el valor inicial de 10 definiendo así un incremento de 10 unidades cada vez que hagamos una llamada a la función incrementer ahora almacenada en incrementByTen.

En este punto quiero remarcar el hecho de que al finalizar la ejecución de esta línea la función incrementByTen ya no tiene acceso al ámbito de makeIncrementer .

Luego en la línea 19 hacemos la primera llamada a la función incrementByTen() obteniendo como resultado el valor de 10 ya que la variable runningTotal fue inicializada con el valor de 0 (cero) y así le siguen dos llamadas más en las líneas siguientes, veamos la salida en pantalla:

10
20
30

…como podemos constatar las siguientes llamadas siguieron aumentando el valor anterior en 10, como es esto posible si ya no podemos acceder al valor de runningTotal?

Esto es posible debido a que las funciones / closure anidados toman las constantes o variables del entorno que las rodea como referencias, es decir una referencia a las mismas, esto permite que luego de terminada la ejecución de makeIncrementer y finalizado el acceso a las variables definidas en su ámbito aún podamos acceder ala información almacenada en las mismas.

Los closures son tipos por referencia

En el ejemplo anterior la constante incrementByTen hace referencia a un closure que incrementa el valor de la variable runningTotal la cual es capturada por referencia, es decir que no se crea una copia nueva, se hace referencia a una única versión de la misma.

Pues resulta que los closures comparten este comportamiento ya que son tipos por referencia, esto quiere decir que cada vez que definimos una variable o constante y la igualamos a una función / closure esta se vuelve automáticamente una referencia a esa función / closure.

Esto al igual que con las variables o constantes capturadas también quiere decir que en memoria tenemos una copia única del closure (incrementer) y la variable (incrementByTen) es solamente una referencia.

Esto en C++ sería un puntero a función, que sería lo mismo que decir un puntero al área de memoria donde se encuentra la función o closure en el caso de Swift. Aparte de la comodidad y flexibilidad que esto nos brinda creo que es más que evidente el hecho de que también nos ayuda con el consumo de memoria.

Dicho eso ¿qué sucedería si hiciéramos esto?

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

    var runningTotal = 0

    func incrementer() -> Int {

        runningTotal += amount

        return runningTotal

    } // incrementer

    return incrementer

} // makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())
print(incrementByTen())
print(incrementByTen())

let alsoIncrementByTen = incrementByTen

print(alsoIncrementByTen())
print(alsoIncrementByTen())
print(incrementByTen())

…la línea 25 es en la que me baso para la pregunta. En esta creamos una constante llamada alsoIncrementByTen y la igualamos con la constante incrementByTen que referencia al closure que incrementa la variable runningTotal.

Al momento de llegar a esta línea la variable runningTotal es igual a 30 por lo que al ejecutar las línea 27 y 29 deberíamos de tener en pantalla los valores de 40 y 50 y así es, pero ¿cual valor obtendríamos al ejecutar la última línea donde nuevamente ejecutamos incrementByTen? pues el valor resultante sería 60, aquí la salida en pantalla:

10
20
30
40
50
60

…lo que sucede en este caso imagino que ya lo supondrán, y la respuesta se encuentra en esta línea:

let alsoIncrementByTen = incrementByTen

…en la cual igualamos una constante con otra y que a su vez es una referencia a un closure, por lo que esta igualdad concluye con el paso de una referencia hacia alsoIncrementByTen, referencia evidentemente común con incrementByTen y que en otras palabras significa que ambas constantes son referencias a una misma posición de memoria donde se encuentra el closure junto a la variable capturada runningTotal.

Por este motivo cuando ejecutábamos alsoIncrementByTen seguíamos incrementando la misma variable runningTotal que cuando ejecutábamos incrementByTen.

En caso de haber sido necesaria una copia nueva y completamente aparte a incrementByTen pudiéramos haber sustituido la línea 25 por la siguiente:

let alsoIncrementByTen = makeIncrementer(forIncrement: 10)

…quedando un código final:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

    var runningTotal = 0

    func incrementer() -> Int {

        runningTotal += amount

        return runningTotal

    } // incrementer

    return incrementer

} // makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen())
print(incrementByTen())
print(incrementByTen())

let alsoIncrementByTen = makeIncrementer(forIncrement: 10)

print(alsoIncrementByTen())
print(alsoIncrementByTen())
print(incrementByTen())

…y una salida como la siguiente:

10
20
30
10
20
40

El atributo @noescape

Cuando declaramos una función que toma un closure como uno de sus argumentos, podemos escribir @noescape antes del nombre del parámetro. Esto nos permite garantizar que el closure no será almacenado en ninguna variable o constante, que no será usado más adelante luego de terminada la ejecución de esa función y que tampoco será usado asincrónicamente. Veamos un ejemplo:

//: Playground - noun: a place where people can play

import UIKit

// Aquí declaramos el Array donde almacenaremos los closures
var arrayOfClosures: [ () -> Void ] = []

func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {

    // La línea siguiente daría como resultado un error ya que el closure está restringido al ámbito de la función y
    // no puede ser almacenado fuera de esta ya que el argumento fue declarado con el atributo @noescape.

    //completionHandlers.append(closure)

    // Ejecutamos el closure
    closure()

} // someFunctionWithNoescapeClosure

func someFunctionWithEscapingClosure(closure: () -> Void) {

    // Almacenamos el closure en el Array
    arrayOfClosures.append(closure)

} // someFunctionWithEscapingClosure

class SomeClass {

    var x = 0

    func doSomething() {

        // En esta llamada estamos obligados a usar self.
        someFunctionWithEscapingClosure { self.x = 100 }

        someFunctionWithNoescapeClosure { x = 200 }

    } // doSomething

    deinit {

        print("SomeClass deinit, goodbye!")

    } // deinit

} // SomeClass

do {

    // Creamos una instancia de nuestra clase
    let instance = SomeClass()

    // Ejecutamos nuestro método
    instance.doSomething()

    // Imprimimos el valor de X
    print(instance.x)

    // Ejecutamos el código del closure que habiamos almacenado
    arrayOfClosures.first?()

    // Imprimimos X nuevamente ahora con un nuevo valor
    print(instance.x)

    // Eliminamos el closure almacenado en el array y con ello el enlace a nuentra instancia de clase
    // para que esta pueda ser liberada.
    //arrayOfClosures.removeAll()

} // do

…en este ejemplo hemos declarado un arreglo, dos funciones y una clase. En el arreglo almacenaremos un closure, las dos funciones las utilizo para demostrar la diferencia entre una cuyo argumento cuenta con el atributo @noescape y la otra que no, luego continuamos con una clase con un método desde donde hacemos las llamadas a las funciones antes comentadas.

En esta misma clase nos encontramos con un bloque deinit (este bloque sería el equivalente al destructor de C++) donde imprimimos un mensaje al momento de ser liberada la memoria ocupada por la clase, por último un bloque do que lo he usado solamente para generar un ámbito para que la clase sea liberada al finalizar este ya que debido a la naturaleza del propio Playground si no hacemos esto el bloque deinit jamás será ejecutado. Veamos la salida en pantalla de este código:

200
100

…tras la impresión de estas líneas nos percatamos que el mensaje:

“SomeClass deinit, goodbye!“

…no ha sido mostrado y esto básicamente es debido a que existe una referencia fuerte a la clase por parte del closure que tenemos almacenado en el arreglo, si descomentamos la línea 67 veremos una nueva salida:

200
100
SomeClass deinit, goodbye!

Con lo que acabo de expresar, más los propios comentarios del código creo que es suficiente para terminar de entender el funcionamiento del mismo, de no ser así pues, en los comentarios de este artículo, me pueden dejar sus dudas.

Algo interesante es que cuando declaramos un closure con este atributo y dadas las características antes comentadas, pues el compilador como ya tiene conocimiento del tiempo de vida del mismo optimiza el código de una manera mucho más agresiva.

El atributo @autoclosure

El atributo @autoclosure nos permite optimizar nuestro código al mismo tiempo que nos facilita la vida ya que este lo podemos aplicar a un argumento de una función que recibe un closure, permitiéndonos a la hora de llamar la función pasar una expresión normal sin la necesidad de las llaves. Este atributo envuelve la expresión y crea de manera automática el closure que la función espera recibir.

Haciendo uso del autoclosure también podemos demorar la evaluación del closure ya que la expresión no se ejecuta hasta que efectuemos su llamada de manera explicita. La posibilidad de demorar la evaluación viene a ser muy útil cuando la expresión se conoce que tiene un impacto negativo en el rendimiento de la aplicación, quizás debido a la carga de información desde una base de datos.

Tener bajo nuestro control cuando una operación así se lleva a cabo nos flexibiliza a actuar de una manera más estratégica y eficiente, nos permite reducir al máximo las molestias que esto pudiera generar al cliente.

En el código a continuación demostramos como podemos demorar la evaluación de un closure:

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

print(customersInLine.count)

let customerProvider = { customersInLine.removeAtIndex(0) }

print(customersInLine.count)

print("Ahora atendiendo a \(customerProvider())!")

print(customersInLine.count)

…la salida en pantalla:

5
5
Ahora atendiendo a Chris!
4

En la primera línea creamos un arreglo con los 5 nombres de una fila de personas que están esperando por ser atendidas, seguimos imprimiendo la cantidad de elementos en el arreglo, en la tercera línea creamos una constante que va a almacenar un closure,  la expresión asignada a esta constante elimina el primer elemento del arreglo simulando que la persona está siendo atendida, y aquí es cuando ocurre lo curioso del caso ya que se está ejecutando el método removeAtIndex(0) mientras que en la salida en pantalla de la línea 4 podemos constatar de que el número de elementos en el arreglo sigue siendo 5.

Con esto comprobamos de que el closure no ha sido ejecutado, en la línea 5 imprimimos un mensaje en la pantalla del local donde informamos a cual cliente estamos atendiendo, obtenemos el nombre del cliente a partir de la constante que almacena el closure, llamada que evidentemente evalua la expresión y la ejecuta, momento donde realmente eliminamos el primer elemento del arreglo como podemos comprobar en la última línea donde el número de elementos ahorita es 4.

Explicado esto optimicemos este código y de paso veamos como implementar todo esto haciendo uso de una función:

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func serveCustomer(customerProvider: () -> String) {

    print("Ahora atendiendo a \(customerProvider())!")

} // serveCustomer

serveCustomer( { customersInLine.removeAtIndex(0) } )

…este código es más compacto y equivalente al anterior pero hagámosle otro cambio:

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func serveCustomer(@autoclosure customerProvider: () -> String) {

    print("Ahora atendiendo a \(customerProvider())!")

} // serveCustomer

serveCustomer(customersInLine.removeAtIndex(0))

En esta versión hemos declarado el argumento de la función con el atributo @autoclosure permitiéndonos al momento de su llamada obviar la declaración del closure de manera explicita, por lo que ahorita podemos interactuar con su argumento como si este tomara un String en lugar de un closure. Como podemos constatar en la última línea escribimos la expresión de manera directa y sin las llaves.

Antes de finalizar analicemos el siguiente ejemplo:

// Arreglo de clientes en espera.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// Arreglo de closures.
var customerProviders: [() -> String] = []

// Función que almacena los closures en el arreglo antes declarado.
func collectCustomerProviders(@autoclosure customerProvider: () -> String) {

    customerProviders.append(customerProvider)

} // collectCustomerProviders

// Almacenamos dos closures que serían equivalente a las dos primeras personas en la fila.
collectCustomerProviders(customersInLine.removeAtIndex(0))
collectCustomerProviders(customersInLine.removeAtIndex(0))

print("Hemos almacenado \(customerProviders.count) closures.")

// Iteramos sobre cada elemento del arreglo de closures al mismo tiempo que hacemos una llamada a los mismos.
for customerProvider in customerProviders {

    print("Ahora atendiendo a \(customerProvider())!")

} // customerProvider

…luego de leer los comentarios en el código y de percatarnos que el compilador nos muestra un error, descubrimos otra característica del atributo @autoclosure y es que este implica @noescape por defecto, es decir que cuando declaramos @autoclosure también se aplican las características del atributo @noescape y esto es lo que justifica el error.

Como podemos observar la función collectCustomerProviders() guarda el closure pasado como argumento en el arreglo customerProviders y esto como ya vimos aparte de crear una referencia fuerte desde la función hacia el arreglo, algo que quizás no sea lo deseado, pues también viola el atributo @noescape ya el argumento está siendo almacenado fuera del ámbito de la función.

Pero si aun sabiendo esto queremos hacer uso del atributo @autoclosure por la comodidad que nos brinda…

¿Qué solución tenemos?

Pues Swift haciendo gala nuevamente de su versatilidad nos permite inhabilitar el comportamiento @noescape del atributo @autoclosure y esto lo logramos con su forma @autoclosure(escaping), veamos como quedaría al código anterior ya corregido:

// Arreglo de clientes en espera.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// Arreglo de closures.
var customerProviders: [() -> String] = []

// Función que almacena los closures en el arreglo antes declarado.
func collectCustomerProviders(@autoclosure(escaping) customerProvider: () -> String) {

    customerProviders.append(customerProvider)

} // collectCustomerProviders

// Almacenamos dos closures que serían equivalente a las dos primeras personas en la fila.
collectCustomerProviders(customersInLine.removeAtIndex(0))
collectCustomerProviders(customersInLine.removeAtIndex(0))

print("Hemos almacenado \(customerProviders.count) closures.")

// Iteramos sobre cada elemento del arreglo de closures al mismo tiempo que hacemos una llamada a los mismos.
for customerProvider in customerProviders {

    print("Ahora atendiendo a \(customerProvider())!")

} // customerProvider

…la salida en pantalla:

Hemos almacenado 2 closures.
Ahora atendiendo a Chris!
Ahora atendiendo a Alex!

Como han podido comprobar, el conocer los closures nos permite movernos con más soltura, ser más creativos y flexibles, aparte de que hemos constatado como hay librerías del propio lenguaje que implementan en sus métodos argumentos en forma de closures, esto nos obliga a dominarlos y sentirnos cómodos con ellos.

Por todo esto los invito a que practiquen mucho, no solamente sobre esté tópico, alguien que no domine un lenguaje en su totalidad no cuenta con las herramientas necesarias para brindar un producto final de alta calidad, su creatividad en cuanto a arquitectura siempre se verá limitada a lo que conoce, cuando quizás ese aspecto que no domina sería la pincelada necesaria para que su proyecto logre sobresalir.

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

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 sugerencia, ya sea errores a corregir, información o ejemplos a añadir será, más que bienvenida, necesaria!

Get real time updates directly on you device, subscribe now.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

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!