Aun trabajo en proyectos que usan el viejo y noble Objective-C y ante un problema para sincronizar varias llamadas a un API REST decidí utilizar la librería PromiseKit que llevaba tiempo queriendo probar. Aprovecho este artículo para explicar su uso básico, que seguramente haya sido explicado miles de veces, pero aportaré aquí mi granito de arena.
Promesas
Una promesa es un objeto que encapsula un valor futuro de una operación asíncrona. El valor es desconocido inicialmente. El ejemplo más habitual es el resultado de una llamada a un API. Inicialmente no sabemos si va a funcionar porque puede fallar la red, el servidor o por ejemplo un cambio en los datos devueltos pueden romper el parseo. En este ejemplo la respuesta prometida es un "futuro" y la forma de obtener esta respuesta es la "promesa"
El concepto viene de la programación funcional y es tremendamente útil para sincronizar la ejecución de los procesos en una aplicación.
PromiseKit es una implementación de este concepto en Swift y Objective-C.
¿Qué aporta el uso de promesas?
El código asíncrono, codificado a través de bloques como en Obj-C moderno y swift es difícil de leer e interpretar. Para empezar hay demasiados corchetes y paréntesis y demasiados niveles de código anidado. El concepto de promesa aporta muchas cosas para resolver este problema:
-
Hace las operaciones asíncronas encadenables en forma de secuencia, en lugar de tener que estar saltando de una parte a otra del código, y por tanto las hace estandarizables y repetibles.
-
Permite ordenar el código asíncrono que en la actualidad vuelve a ser, por desgracia, código espagueti con montones de bloques anidados.
-
Simplifica el manejo de errores en la cadena de llamadas. Por ejemplo, la mayoría de llamadas a un API devuelven datos en un bloque de éxito o un error en un bloque de error. Si creamos una secuencia de llamadas podemos unificar el tratamiento de los errores.
-
Permite resolver el típico problema de hacer un cambio en UI desde un hilo secundario de forma elegante. Todo el código ejecutado en un bloque "then" se ejecuta siempre en el hilo principal.
Uso básico en Objective-C
Es sencillo instalar PromiseKit usando CocoaPods o Carthage siguiendo las instrucciones en su web. Una vez hecho esto, para poder usarlas en Objective-C debemos importar AnyPromise.h.
#import "AnyPromise.h"
Para crear una promesa tenemos que crear un método que devuelva AnyPromise. Por ejemplo, para modificar el típico método para consumir un API REST:
+ (void)getAPIDataWithParam:(NSString *)param
success:(APIObjectResult)success
failure:(APIErrorHandler)failure;
Se cambiaría a:
+ (AnyPromise *)getAPIDataWithParam:(NSString *)param;
Dentro de este método tendremos que resolver la promesa en un momento dado, bien con datos o bien con un error, con lo que decimos que estamos rechazando la promesa. Para eso, este método deberá incluir una llamada a promiseWithResolverBlock:
+ (AnyPromise *)getAPIDataWithParam:(NSString *)param {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
// do sth
resolve(data);
// or
// resolve(error);
}
}
Para consumir la promesa usaremos la semántica básica then…catch
AnyPromise *apiDataPromise = [APIRestWrapper getAPIDataWithParam:param];
apiDataPromise.then(^{
// Do sth
}).catch(^(NSError *error){
// Process error
});
Extensiones de APIs de Cocoa
Además de la implementación del concepto de promesa, PromiseKit ofrece una serie de extensiones de los APIs de Apple más habituales y alguna otra como AlamoFire.
Algunos de los segmentos de código que pongo a continuación usan estas extensiones.
Chaining
La herramienta básica en PromiseKit es el bloque then que permite encadenar promesas. De esta forma podemos expresar en forma de secuencia varios procesos asíncronos.
Por ejemplo:
[NSURLConnection promise:rq1].then(^(id data1){
return [NSURLConnection promise:rq2];
}).then(^(id data2){
return [NSURLConnection promise:rq3];
}).then(^(id data3){
// Do sth else
});
Paso de datos
Podemos pasar hasta tres valores (o ninguno) en los bloques de promesas pero lo normal es que pasemos solo un valor. El valor con el que resolvemos la promesa llega a través del bloque then.
myPromise.then(^(id data){
// Do sth with Data
});
Bloques que se ejecutan siempre: Always
Si necesitamos ejecutar cierta lógica siempre al final de nuestra secuencia podemos hacerlo con el bloque always. Se puede aprovechar para lanzar una actualización de UI que siempre se tenga que hacer tras acabar una serie de llamadas al API, por ejemplo.
...
}).then(^(id data){
...
}).always(^{
// Update UI
}
Error Handling: Catch
Un concepto importante sobre como gestiona PromiseKit los errores es que los errores son propagados a lo largo de una secuencia de promesas hasta que se llega al bloque catch donde se pueden procesar todos.
Por ejemplo:
[NSURLConnection GET:url].then(^(NSDictionary *json){
return [NSURLConnection GET:json[@"avatar_url"]];
}).then(^(UIImage *image){
self.imageView.image = image;
}).catch(^(NSError *error){
[[UIAlertView …] show];
})
Se puede poner un catch en cualquier punto en un encadenamiento de promesas, no solo al final. Esto nos permite recuperar un error en un catch que no sea fatal y encadenar a continuación un then.
[CLLocationManager promise].catch(^id(NSError *error){
if (error.code == CLLocationUnknown) {
return CLLocationChicago;
} else {
return error;
}
}).then(^(CLLocation *userLocation){
// the user’s location, or Chicago for specific errors
}).catch(^{
// errors that were not recovered above
});
Cuando devolvemos en un catch algo que no sea un NSError la ejecución continuará. en estos casos se cambia el tipo del bloque retornado en catch de NSError a id.
Sincronizar varias promesas para la ejecución de otra: when y join
Cuando queremos crear flujos en los que hay varias promesas que tienen que resolverse para dar inicio a una tercera, usaremos when (PMKWhen)
Por ejemplo:
id search1 = [[[MKLocalSearch alloc] initWithRequest:rq1] promise];
id search2 = [[[MKLocalSearch alloc] initWithRequest:rq2] promise];
PMKWhen(@[search1, search2]).then(^(NSArray *results){
//…
}).catch(^{
// called if either search fails
});
Si cualquiera de las promesas de las que se depende es rechazada, la promesa devuelta por when es rechazado con un error inmediatamente, sin esperar a que finalicen todas. Si queremos esperar a que todas sean resueltas, podemos usar join (PMKJoin)
Promesas opcionales en una secuencia
Este es el caso en que tenemos una serie de promesas pero alguna de ellas es opcional y no es la última en la secuencia. Es decir, puede no tener que ejecutarse por ejemplo porque la llamada al servicio REST solo se hace si se cumple cierta condición. En este caso necesitamos seguir con la secuencia y para hacerlo podemos devolver una promesa vacía:
return [AnyPromise promiseWithValue: nil];