Async await Swift 5.5
Con la llegada de Swift 5.5 se introdujo una de las características del lenguaje que los desarrolladores llevábamos más tiempo esperando, async/await, similar como se lo hace en C# y JavaScript. El cual nos permite tratar código asíncrono como si fuese síncrono, lo cual aumenta la calidad del código en gran medida.
Para entender esto, debemos revisar con un ejemplo como se hacia antes este tipo de tareas. Imaginemos que necesitamos traer datos de un usuario de un servidor y guardar esos datos.
func fetchUser(completion: @escaping (String) -> Void) {
DispatchQueue.global().async {
let result = "Antoni"
completition(result)
}
}
func save(result: String, completion: @escaping (String) -> Void) {
// More complex networking code; we'll just send back "OK"
DispatchQueue.global().async {
completion("Saved")
}
}
// y para usarlo lo hariamos de esta manera:
fetchUser { user in
save(result: user) { response in
print(response)
}
}
Con este tipo de código, puede existir varios problemas como:
Es posible que esas funciones llamen a su controlador de finalización más de una vez u olviden llamarlo por completo.
La sintaxis del parámetro @escaping (String) -> Void puede ser difícil de leer.
Con este código estamos creando una estructura en forma de pirámide cada vez mas grande dependiendo de cuantas cosas debemos hacer con los datos lo que incrementa muchas lineas de código y se hace difícil de entender.
Desde Swift 5.5, ahora podemos limpiar nuestras funciones marcándolas para que devuelvan un valor de forma asincrónica en lugar de depender de controladores de finalización.
Un ejemplo seria similar al anterior, pero esta vez traeremos varios usuarios del servidor.
enum UserError: Error {
case invalidCount, dataTooLong
}
func fetchUsers(count: Int) async throws -> [String] {
if count > 3 {
// Es un ejemplo de un error que si tratan de traer mas de 3 users mostrara un error.
throw UserError.invalidCount
}
// la logica de traer y retornar los usuarios estaria aquí, en este ejemplo solo retornaremos una lista de usuarios basados en el parámetro de la función.
return Array(["Antoni", "Karamo", "Tan"].prefix(count))
}
func save(users: [String]) async throws -> String {
let savedUsers = users.joined(separator: ",")
if savedUsers.count > 32 {
throw UserError.dataTooLong
} else {
// el codigo de guardar los users estaria aquí
return "Saved \(savedUsers)!"
}
}
Como puede ver, todos los cierres se han ido, creando lo que a veces se llama "código de línea recta": aparte de la palabra clave await, parece un código síncrono.
La adición de async/await encaja perfectamente con try/catch, lo que significa que las funciones asíncronas y los inicializadores pueden arrojar errores si es necesario.
Para usar todo el código anterior dentro de una sola función lo haríamos de esta manera:
func updateUsers() async {
do {
let users = try await fetchUsers(count: 3)
let result = try await save(users: users)print(result)
} catch {
print("Oops, something went wrong!")
}
}
Nota: Es posible que haya notado que cada función que llama a una función asíncrona también debe marcarse con la palabra clave asíncrona. En el momento en que marca una función como asíncrona, todas las funciones que la llaman también deben ser asíncronas, extendiéndose rápidamente a través de su base de código.
Sin embargo, la mayor parte del código que escribimos es síncrono. Necesitamos una forma de superar la barrera entre el código síncrono y el asíncrono e interrumpir la infinita cadena ascendente de funciones asíncronas.
Para resolver esto usaremos Task.
Task crea un entorno en el que puede ejecutar y administrar el trabajo asíncrono en un subproceso separado. Puede ejecutar, pausar y cancelar código asincrónico a través del tipo de tarea.
Entonces podemos cambiar la función de esta manera para utilizarla en cualquier otra función sin necesidad de cambiar dicha función a asíncrona.
func updateUsers() {
Task {
do {
let users = try await fetchUsers(count: 3)
let result = try await save(users: users)print(result)
} catch {
print("Oops, something went wrong!")
}
}
}