Como ejecutar operaciones en paralelo con Swift?

Las llamadas http desde una aplicación mobile son el pan de cada día y mas de una vez nos hemos encontrado con que tenemos que consultar el estado de múltiples elementos en paralelo a través de una api y reaccionar a este resultado, en este tutorial estaremos presentando las diferentes formas que podremos realizar múltiples llamadas http al mismo tiempo y recibir dicho resultado.

En este tutorial estamores analizando 3 de los metodos mas comunes para poder logar este comportamiento:

  1. Global Central Dispatch (GCD)
  2. Goolge Promises Libreria
  3. RxSwift

Para nuestros ejemplos estaremos consultado esta API (World Time API) que simplemente para decirnos la hora en diferentes regiones de mundo. 

Global Central Dispatch (GCD)

Este metodo podriamos decir que es el mas facil y simple, dado que no tenemos que instalar ninguna libreria. Para poder lograr lo que queremos tenemos que hacer uso de la clase DispatchGroup una clase que de por si no hace mucho, pero nos permite saber cuando una serie de tareas fueron iniciadas y completadas.

Enter / Leave

Todo el trabajo de DispatchGroup radica en 2 metodos, dispatchgroup.enter() y dispatchgroup.leave()

let group = DispatchGroup()
    
let urls = [
    "http://worldtimeapi.org/api/timezone/America/Chicago",
    "http://worldtimeapi.org/api/timezone/Europe/London",
    "http://worldtimeapi.org/api/timezone/Europe/Rome"
]

urls.forEach { (url) in
    self.group.enter()
    
    httpCall(url: url) { (result) in
    print(result)

    self.group.leave()
    }
}

self.group.notify(queue: DispatchQueue.main) {
    print("Todas las tareas fueron completadas")
}

Para que no tengamos una excepcion, necesitaremos realizar el mismo numero de llamadas de enter y lleave.

func httpCall(url: String, completion: @escaping (_: [String: Any]) -> Void) {
    let url = URL(string: url)!

    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
      guard let data = data else { return }

      do {
        let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
          
        completion(json ?? [:])
      } catch {
          print("JSON error: \(error.localizedDescription)")
        completion([:])
      }
    }

    task.resume()
  }

Si en caso necesitaran el resultado de todas las llamadas http son un simple cambio al script anterior podria ser logrado, solo tendriamos que de alguna forma guardar todas los resultados con identificador, en este caso la url que esta realizando la llamada:

let group = DispatchGroup()
var results: [String: Any] = [:]

let urls = [
  "http://worldtimeapi.org/api/timezone/America/Chicago",
  "http://worldtimeapi.org/api/timezone/Europe/London",
  "http://worldtimeapi.org/api/timezone/Europe/Rome"
]

urls.forEach { (url) in
  self.group.enter()
  
  httpCall(url: url) { (result) in
    results[url] = result
    
    self.group.leave()
  }
}

self.group.notify(queue: DispatchQueue.main) {
  print("Todas las tareas fueron completadas")
}

Una nota con respecto a estas llamadas es que no estoy tomando en cuenta los posibles errores con el hecho de mantener simple el ejemplo.

Google Promises

Google Promises es una libreria al parecer creada por google que sirve para trabajar con operaciones asincronas en el ecosistema iOS, la misma se puede usar en objective al igual que Swift, en el mercado hay varios soluciones de este tipo, pero su simpleza y su semejanza con las promses de Javascript lo hacen seleccionarlo para mis desarrollos. Aparte tienen esta prueba de rendimiento.

Para comenzar a trabajar con google promises tenemos primero que intalarlo, y eso lo logramos simplemente agregando esto a nuestro podfile.

pod 'PromisesSwift'

y pod install en nuestra terminal.

Para poder realizar cualquier tarea utilizando promesas tendremos que adaptar nuestro codigo a trabajar con promesas para poder hacer uso de la lib como tal. En el ejemplo anterior hicimos una llamada http y en este caso haremos lo mismo pero adaptaremos para retornar una promesa en cada llamada de la funcion en vez de pasar un completion block.

 

func promisesCall(url urlString: String) -> Promise<[String: Any]> {
  return Promise<[String: Any]> { fulfill, reject in
    let url = URL(string: urlString)!

    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
      guard let data = data else { return }

      do {
        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
          fulfill(json)
        }
      } catch {
          print("JSON error: \(error.localizedDescription)")
        reject(error)
      }
    }

    task.resume()
  }
}

una vez realizado esto podemos dar uso de la funcion Promises.all, que toma una lista de promesas y las ejecuta.

Promise.all

let urls = [
  "http://worldtimeapi.org/api/timezone/America/Chicago",
  "http://worldtimeapi.org/api/timezone/Europe/London",
  "http://worldtimeapi.org/api/timezone/Europe/Rome"
]

Promises.all(urls.map({ promisesCall(url: $0) }))
  .then { (resultados) -> Void in
    print("Resultados promeses", resultados)
  }.catch { (error) in
    print("Error")
  }

De esta forma podemos realizar varias operaciones y ser notificados una vez todas estas esten completas.

 

RXSwift

El ultimo pero no menos importante, esta esta super libreria que practicamente hace de todo, pero lo que nos interesa en esta ocacion es el poder ejecutar varias tareas en paralello y ser notificados cuando todo este completo.

Para intalarlo tendremos que coloar esto en nuestro archivo pod file


pod 'RxSwift', '~> 5'

Para poder sacar provecho a rxSwift y las tareas que necesitamos ejecutar tendremos que envolver nuestras operaciones (http calls) en Observables que es lo que maneja RxSwift.

func reactiveCall(url urlString: String) -> Observable<[String: Any]> {
  return Observable.create { (observer) -> Disposable in
    
    let url = URL(string: urlString)!

    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
      guard let data = data else { return }

      do {
        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
          observer.onNext(json)
        }
      } catch {
          print("JSON error: \(error.localizedDescription)")
        
        observer.onError(error)
      }
    }

    task.resume()
    
    return Disposables.create()
  }
}

Simplemente cambiamos el completion handler por una llamada del observer.onNext y en caso de un error Observer.onError.

Como el objetivo de este tutorial es ejecutar varias operaciones y obtener el resulto de todas estas al mismo tiempo, estamos dando uso a la funcion Observable.zip que simplemente hablando toma una lista de Observable y retorna el una lista de los resultados de todos estos observables.

let disposeBag = DisposeBag()
let urls = [
  "http://worldtimeapi.org/api/timezone/America/Chicago",
  "http://worldtimeapi.org/api/timezone/Europe/London",
  "http://worldtimeapi.org/api/timezone/Europe/Rome"
]

Observable.zip(urls.map({ reactiveCall(url: $0) }))
  .subscribe(onNext: { (results) in
    print("Reactive results ", results)
  }, onError: { (error) in
    print("Error", error)
  })
  .disposed(by: disposeBag)

Bueno, aqui tenemos 3 formas de como realizar la misma tarea, podriamos decir que todas cumple con el trabajo y a la hora de decidir cual metodo usar yo en lo personal usaria el que ya mi proyecto este usando, si mi projecto ya tiene rxSwift ese es el que tendria que usar.

Referencias

RxSwift
Google Promises
DispatchGroup GCG