Consumiendo Google desde iOS (4) – Hola Mundo por @vermicida

Autor: | Última modificación: 9 de abril de 2024 | Tiempo de Lectura: 9 minutos
Temas en este post: ,
Con este capítulo terminamos el tutorial usando Google desde iOS (4). ¿Qué nos queda? Hacer el hola mundo de rigor consumiendo un recurso de Google. He elegido Google Calendar porque creo que es una herramienta que muchos usamos, por lo que de antemano contamos con una batería de datos con los que jugar y ver resultados de manera inmediata. Lo primero que haremos será pasar por la API de Google Calendar para consultar cómo debemos autorizar las peticiones al servicio. Y ya que estamos, dejaremos a mano la referencia de la API, que la necesitaremos en breve. Los otros capítulos son:

Hola mundo

Debéis saber que el diseño no es lo mío, así que nuestra prueba de concepto va a ser un soberano esperpento. Y dicho esto, abrimos Xcode y recuperamos nuestro proyecto. Vamos a añadir un par de objetos a nuestra interfaz, por lo que buscamos el documento Main.storyboard en Project Navigator y lo seleccionamos. Sobre la vista existente, añadimos un objeto Table View. Sobre la tabla, un objeto Toolbar. Y sobre la barra, un par de objetos Bar Button Item y un objeto Flexible Space Bar Button Item. Uno de los botones iniciará el flujo de autorización OAuth 2.0, mientras que el otro revocará el token de acceso vigente -en caso de tenerlo- concedido previamente; usamos como títulos Grant y Revoke respectivamente. Deberíamos tener algo parecido a esto:
Storyboard_Google desde iOS
La interfaz que necesitamos. La conocéis de mil tutoriales 😉
Vamos a seleccionar la vista de asistente, la del icono del esmoquin; a un lado debemos tener el storyboard, y al otro el documento de definición -el .h– del controlador asociado. En mi caso se llama DHViewController. Tenemos que definir los outlets y actions de los botones; como siempre, lo haremos arrastrando con el ratón -y con ctrl pulsado- desde cada botón en el storyboard hasta el .h del controlador. Lo hacemos una vez para definir el outlet, y otra más para el action. Con sendos botones.
Outlets y actions_Google desde iOS (4)
Importante: arrastramos el ratón desde el botón hasta el .h con la tecla ctrl pulsada.
Aprovechamos antes de salir de la vista de asistente para añadir también el outlet de la tabla. Nuestro .h luce ahora de esta forma -o con los nombres que hayáis usado-:
#import <UIKit/UIKit.h>

@interface DHViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIBarButtonItem *grantButton;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *revokeButton;
@property (weak, nonatomic) IBOutlet UITableView *table;

- (IBAction)grant:(id)sender;
- (IBAction)revoke:(id)sender;

@end
Para interactuar con la tabla usaremos los protocolos UITableViewDelegate y UITableViewDataSource; más tarde implementaremos algunos mensajes de éstos. ¿No echáis nada en falta hasta ahora? Efectivamente, nos falta nuestro super delegado, el que creamos en el capítulo anterior: DHGoogleOauthDelegate. Hay que importar el documento de definición para poder usarlo. Añadimos todo lo comentado para dejar nuestra @interface en el .h de esta forma:
// ...

#import "DHGoogleOauth.h"

@interface DHViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, DHGoogleOauthDelegate>

//...

@end
¿Qué va a hacer exactamente nuestro hola mundo? Listar nuestros calendarios, ni más ni menos. Ah, pero, ¿puedo tener más de uno? Claro que sí; un caso práctico, por ejemplo, el mío. Yo tengo mi propio calendario, y además estoy subscrito al de cada uno de los miembros de mi equipo, al de mi jefe de proyecto, y al de los días festivos nacionales; de un vistazo sé cuándo puedo convocar reuniones en huecos que todos tengamos libres, conocer las vacaciones de cada uno para tenerlo en cuenta en las estimaciones de los proyectos, etc. Por tanto, saber cuáles son nuestros calendarios puede ser muy útil. ¡Nos ponemos manos a la obra! Ahora que sabemos que vamos a trabajar con una colección de calendarios, antes de dejar atrás nuestro querido .h, vamos a crear una @property para facilitarnos el trabajo. Dentro de @interface, junto a los outlets que creamos antes:
@property (nonatomic, strong) NSMutableArray *calendars;
Ahora sí, toca moverse al documento de implementación –el .m– para codificar un poco más a fondo. Necesitamos tener una @property dedicada a nuestra super clase DHGoogleOauth; recordemos que la idea es que todo el flujo de autorización OAuth 2.0 lo lleve a cabo ella, así como la autorización de las peticiones HTTP a las APIs de Google. Por tanto, justo antes de @implementation, añadimos:
@interface DHViewController ()

@property (nonatomic, strong) DHGoogleOauth *googleOauth;

@end
Cuando la aplicación se inicie y se muestre la vista asociada al controlador, debemos establecer el comportamiento de los distintos elementos que hemos ido añadiendo. Lo hacemos en el mensaje viewDidLoad. Debemos tener a mano los datos del servidor de autorización; recordad que los tenemos en la Cloud Console, en la sección APIs & auth > Credentials del proyecto que creamos en el primer capítulo. Además, necesitaremos el scope de Google Calendar que vamos a consumir; en el enlace que os dejé más arriba sobre cómo autorizar las peticiones se ofrecen dos:
  • https://www.googleapis.com/auth/calendar para acceso total a los calendarios.
  • https://www.googleapis.com/auth/calendar.readonly para acceso de solo lectura a los calendarios.
El segundo es más que suficiente para nuestro propósito. Por tanto, escribimos:
- (void)viewDidLoad {

    [super viewDidLoad];

    // Le decimos a la tabla que este controlador es su delegado y su fuente de datos.
    [[self table] setDelegate:self];
    [[self table] setDataSource:self];

    // Iniciamos nuestra propiedad googleOauth con los datos correspondientes al servidor de autorización y al scope que vamos a consumir.
    [self setGoogleOauth:[[DHGoogleOauth alloc] initWithClientId:@"vuestro_client_id"
                                                 andClientSecret:@"vuestro_client_secret"
                                                  andRedirectUri:@"urn:ietf:wg:oauth:2.0:oob"
                                                       forScopes:@[@"https://www.googleapis.com/auth/calendar.readonly"]
                                                    inParentView:[self view]]];

    // Avisamos al googleOauth de que este controlador es su delegado.
    [[self googleOauth] setGoogleOauthDelegate:self];

    // Instanciamos el array de calendarios.
    [self setCalendars:[[NSMutableArray alloc] init]];

    // Establecemos el botón Revoke como deshabilitado.
    [[self revokeButton] setEnabled:NO];
}
Vamos con los mensajes de los delegados de la tabla. En este caso, la tabla no va a estar dividida en secciones, y se lo debemos indicar cuando envíe el mensaje numberOfSectionsInTableView. El número de filas vendrá dado por el número de calendarios a los que estemos subscritos en Google Calendar; debemos facilitarle este dato a la tabla en el mensaje tableView:numberOfRowsInSection. Para el tercer mensaje, tableView:cellForRowArIndexPath, vamos a necesitar tirar de chuleta. Este mensaje se encarga de pintar las celdas de cada fila de la tabla con la información que se crea oportuna. Lo que haremos será escribir en cada celda el título del calendario en cuestión y también una descripción del mismo -si es que existe tal cosa-. Un momento: esta información, ¿nos la da la API de Google Calendar? Vamos a comprobarlo consultando la referencia del objeto calendarList. ¡Premio! Sí que nos da esa información. En el JSON de respuesta tenemos entre otros:
  • id, el identificador del calendario.
  • summary, el título del calendario.
  • description, la descripción del calendario -opcional-.
Ahora que tenemos esta información, la llevamos al código:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return [[self calendars] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *identifier = @"document_cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];

    if (cell == nil) {

        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                      reuseIdentifier:identifier];

        [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
        [cell setAccessoryType:UITableViewCellAccessoryNone];

        [[cell textLabel] setFont:[UIFont fontWithName:@"Trebuchet MS"
                                                  size:13.0]];
        [[cell detailTextLabel] setFont:[UIFont fontWithName:@"Trebuchet MS"
                                                        size:11.0]];
    }

    NSDictionary *calendar = [[self calendars] objectAtIndex:[indexPath row]];

    [[cell textLabel] setText:[calendar objectForKey:@"summary"]];
    [[cell detailTextLabel] setText:[calendar objectForKey:@"description"] == nil
        ? [calendar objectForKey:@"id"]
        : [calendar objectForKey:@"description"]];

    return cell;
}
En este punto ya hemos configurado el inicio de la vista y establecido el comportamiento de la tabla. ¿Qué nos queda? ¡Nuestros amigos los botones! Vamos a programar los actions de Grant y Revoke, que va a ser tan sencillo como esto:
- (IBAction)grant:(id)sender {

    [[self googleOauth] grantAuthorization];
}

- (IBAction)revoke:(id)sender {

    [[self googleOauth] revokeAuthorization];
}
Bueno, llagados aquí, nuestra aplicación ya está acabada. Vamos a correr el proyecto desde Xcode y ver qué pasa:
Google Oauth iOS Simulator 01
¡Vamos por buen camino! Pulsamos Grant.
Google Oauth iOS Simulator 02
Debemos iniciar sesión con nuestra cuenta de Google.
Google Oauth iOS Simulator 03
Pues ha quedado bien la pantalla de consentimiento que configuramos…
Google Oauth Console Error
Ups, ¿qué ha pasado?
¡Claro! Soy un desastre. Si no implementamos los mensajes de DHGoogleOauthDelegate mal vamos. Bueno, vamos a ponerle remedio. Una vez que hemos autorizado a nuestra aplicación iOS el uso del scope de Google Calendar con nuestra cuenta de usuario, lo que queremos es que nos muestre la colección de calendarios. Pero, como ya hemos comentado, las peticiones a las APIs deben ir autorizadas con el token de acceso que nos han otorgado. ¿Cómo hacemos esto? Pues vamos a completar un poco más nuestra super clase DHGoogleOauth; añadiremos cuatro nuevos mensajes para hacer peticiones HTTP autorizadas a las APIs. ¿Por qué cuatro? Una para cada verbo HTTP: GET, POST, PUT y DELETE. Vamos a ello. En el .h de DHGoogleOauth:
- (void)doGetRequest:(NSString *)url
      withParameters:(NSDictionary *)parameters
       thenOnSuccess:(RequestSuccess)successBlock
         thenOnError:(RequestError)errorBlock;

- (void)doPostRequest:(NSString *)url
       withParameters:(NSDictionary *)parameters
        thenOnSuccess:(RequestSuccess)successBlock
          thenOnError:(RequestError)errorBlock;

- (void)doPutRequest:(NSString *)url
      withParameters:(NSDictionary *)parameters
       thenOnSuccess:(RequestSuccess)successBlock
         thenOnError:(RequestError)errorBlock;

- (void)doDeleteRequest:(NSString *)url
         withParameters:(NSDictionary *)parameters
          thenOnSuccess:(RequestSuccess)successBlock
            thenOnError:(RequestError)errorBlock;
En el .m los implementamos:
- (void)doGetRequest:(NSString *)url
      withParameters:(NSDictionary *)parameters
       thenOnSuccess:(RequestSuccess)successBlock
         thenOnError:(RequestError)errorBlock {

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager GET:url
      parameters:[self tokenizeParameters:parameters]
         success:^(AFHTTPRequestOperation *operation, id responseObject) {
             successBlock( responseObject );
         }
         failure:^(AFHTTPRequestOperation *operation, NSError *error) {
             errorBlock( error );
         }];
}

- (void)doPostRequest:(NSString *)url
      withParameters:(NSDictionary *)parameters
       thenOnSuccess:(RequestSuccess)successBlock
         thenOnError:(RequestError)errorBlock {

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager POST:url
      parameters:[self tokenizeParameters:parameters]
         success:^(AFHTTPRequestOperation *operation, id responseObject) {
             successBlock( responseObject );
         }
         failure:^(AFHTTPRequestOperation *operation, NSError *error) {
             errorBlock( error );
         }];
}

- (void)doPutRequest:(NSString *)url
      withParameters:(NSDictionary *)parameters
       thenOnSuccess:(RequestSuccess)successBlock
         thenOnError:(RequestError)errorBlock {

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager PUT:url
      parameters:[self tokenizeParameters:parameters]
         success:^(AFHTTPRequestOperation *operation, id responseObject) {
             successBlock( responseObject );
         }
         failure:^(AFHTTPRequestOperation *operation, NSError *error) {
             errorBlock( error );
         }];
}

- (void)doDeleteRequest:(NSString *)url
      withParameters:(NSDictionary *)parameters
       thenOnSuccess:(RequestSuccess)successBlock
         thenOnError:(RequestError)errorBlock {

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager DELETE:url
      parameters:[self tokenizeParameters:parameters]
         success:^(AFHTTPRequestOperation *operation, id responseObject) {
             successBlock( responseObject );
         }
         failure:^(AFHTTPRequestOperation *operation, NSError *error) {
             errorBlock( error );
         }];
}
Si os habéis fijado, los parámetros no se pasan directamente al AFHTTPRequestOperationManager, sino que se previamente se les adjunta la información del token de acceso. Vamos a codificar el mensaje tokenizeParameters. En el mismo .m, arriba del todo, dentro de la @interface:
- (NSDictionary *)tokenizeParameters:(NSDictionary *)parameters;
Y la implementación correspondiente:
- (NSDictionary *)tokenizeParameters:(NSDictionary *)parameters {

    NSMutableDictionary *tokenized = parameters == nil
        ? [[NSMutableDictionary alloc] init]
        : [NSMutableDictionary dictionaryWithDictionary:parameters];

    [tokenized setValue:[[self token] objectForKey:@"access_token"]
                                            forKey:@"access_token"];

    return [tokenized copy];
}
Ahora que ya tenemos nuestra interfaz para realizar peticiones HTTP autorizadas, nos volvemos al documento .m del controlador DHViewController para implementar los mensajes de DHViewControllerDelegate que nos faltan. Son muy sencillos, en el mismo código quedan comentados. Para la solicitud de los calendarios, recurrimos de nuevo a la chuleta de la API. Esta vez, nos fijaremos en la acción list del objeto calendarList.
- (void)authorizationGranted {

    NSLog( @"Authorization granted" );

    // Deshabilitamos el botón Grant, y habilitamos Revoke.
    [[self grantButton] setEnabled:NO];
    [[self revokeButton] setEnabled:YES];

    // Definimos el bloque encargado de manejar las peticiones de calendarios satisfactorias.
    // Lo que hacemos es almacenar la respuesta en el array que definimos previamente y recargar la tabla para mostrarlos.
    RequestSuccess onSuccess = ^(id response) {

        [[self calendars] addObjectsFromArray:[response objectForKey:@"items"]];

        NSLog( @"%@", [self calendars] );

        [[self table] reloadData];
    };

    // Definimos el bloque encargado de manejar las peticiones de calendarios fallidas.
    RequestError onError = ^(NSError *error) {

        NSLog( @"Error occured: %@", [error localizedDescription] );
    };

    [[self googleOauth] doGetRequest:@"https://www.googleapis.com/calendar/v3/users/me/calendarList"
                      withParameters:nil
                       thenOnSuccess:onSuccess
                         thenOnError:onError];
}

- (void)authorizationRevoked {

    NSLog( @"Authorization revoked" );

    // Limpiamos el array de calendarios y vaciamos la tabla.
    [[self calendars] removeAllObjects];
    [[self table] reloadData];

    // Habilitamos el botón Grant, y deshabilitamos Revoke.
    [[self grantButton] setEnabled:YES];
    [[self revokeButton] setEnabled:NO];
}

- (void)errorOccured:(NSError *)error {

    NSLog( @"Error occured: %@", [error localizedDescription] );
}
Ahora parece que sí, que la aplicación está terminada. ¿La probamos de nuevo?
Google Oauth iOS Simulator 05
Y por fin, aquí están nuestros calendarios. Pixelados…
Hasta aquí llega lo relacionado con el consumo de APIs de Google desde iOS, pero vamos a darle una última vuelta de tuerca a nuestra aplicación. Esto es por dejarla un poco menos sosa, pero no es necesario en absoluto. Los calendarios de Google Calendar tienen asociado un color de fondo y otro para el texto. Si tuvisteis la picardía suficiente hace unos minutos al mirar la referencia del objeto calendarList, veríais que el JSON de respuesta que almacena la información de cada calendario también nos aporta estos colores. El color de fondo viene dado en la propiedad backgroundColor, y el color del texto en foregroundColor. En ambos casos, el formato del color es hexadecimal. ¡Vaya hombre! Nuestro amigo UIColor no sabe de colores hexadecimales. ¿Qué hacemos ahora? Pues soluciones hay varias, pero a mi juicio, la más elegante se llama categoría. Vamos a crear una categoría para el señor UIColor, y le vamos a llamar UIColor+HexColor, por ejemplo. En el .h definimos el inicializador que hará la magia:
+ (UIColor *)colorWithHexString:(NSString *)hexColor
             defaultWhenUnknown:(UIColor *)defaultColor;
Y en el .m implementamos dicha magia:
+ (UIColor *)colorWithHexString:(NSString *)hexColor
             defaultWhenUnknown:(UIColor *)defaultColor {

    UIColor *color;

    NSString *trimmed = [[hexColor stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];

    if ( [trimmed length] < 6 ) {

        color = defaultColor;
    }
    else {

        if ( [trimmed hasPrefix:@"#"] ) {

            trimmed = [trimmed substringFromIndex:1];
        }

        if ( [trimmed length] != 6 ) {

            color = defaultColor;
        }
        else {

            unsigned int red;
            unsigned int green;
            unsigned int blue;

            NSRange range;
            range.length = 2;

            // Red
            range.location = 0;
            [[NSScanner scannerWithString:[trimmed substringWithRange:range]] scanHexInt:&red];

            // Green
            range.location = 2;
            [[NSScanner scannerWithString:[trimmed substringWithRange:range]] scanHexInt:&green];

            // Blue
            range.location = 4;
            [[NSScanner scannerWithString:[trimmed substringWithRange:range]] scanHexInt:&blue];

            color = [UIColor colorWithRed:((float)red / 255.0f)
                                    green:((float)green / 255.0f)
                                     blue:((float)blue / 255.0f)
                                    alpha:1.0f];
        }
    }

    return color;
}
De vuelta al .m del controlador DHViewController, vamos al mensaje tableView:cellForRowAtIndexPath y justo después de establecer el texto de la celda -y por supuesto antes del return- añadimos lo siguiente. No os olvidéis añadir la referencia a la categoría:
#import "UIColor+HexColor.h"

//...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    //...

    [cell setBackgroundColor:[UIColor colorWithHexString:[calendar objectForKey:@"backgroundColor"]
                                      defaultWhenUnknown:[UIColor whiteColor]]];
    [[cell textLabel] setTextColor:[UIColor colorWithHexString:[calendar objectForKey:@"foregroundColor"]
                                            defaultWhenUnknown:[UIColor blackColor]]];
    [[cell detailTextLabel] setTextColor:[UIColor colorWithHexString:[calendar objectForKey:@"foregroundColor"]
                                                  defaultWhenUnknown:[UIColor blackColor]]];

    return cell;
}
Probamos, una vez más.
Google Oauth iOS Simulator 06
Ríete tú de la bandera gay con semejante explosión de color.
¡Voilá! Ahora sí que está llamativa la aplicación. Bueno, con esto terminamos nuestro tutorial. Espero que os ayude en algún sentido, ya sea para comprender cómo funciona el flujo de autorización OAuth 2.0, o cómo se consumen las APIs de Google una vez autorizados. Es un tutorial bastante sencillo que puede llevar un par de horas realizarlo paso a paso, pero si os pica la curiosidad, acabaréis probando las APIs de otros servicios y tiraréis días y días. Solo diré: cuidado, que engancha.

Posts Relacionados