Crea una aplicación meteorológica con Forecast: Integración de la API
Spanish (Español) translation by steven (you can also view the original English article)
En el primer artículo de esta serie, sentamos las bases del proyecto configurando el proyecto y creando la estructura de la aplicación. En este artículo, aprovechamos la biblioteca AFNetworking para interactuar con la API Forecast.
Introducción
En la primera entrega de esta serie, sentamos las bases de nuestra aplicación meteorológica. Los usuarios pueden agregar su ubicación actual y cambiar entre ubicaciones. En este tutorial, usaremos la biblioteca AFNetworking para pedirle a la API Forecast los datos meteorológicos de la ubicación seleccionada actualmente.
Si deseas continuar, necesitarás una clave API de Forecast. Puedes obtener una clave API registrándote como desarrollador en Forecast. Registrarse es gratis, así que te animo a que pruebes el servicio meteorológico Forecast. Puedes encontrar tu clave de API en la parte inferior de tu panel (figura 1).



1. Subclasificación de AFHTTPClient
Como escribí anteriormente en este artículo, utilizaremos la biblioteca AFNetworking para comunicarnos con la API de Forecast. Hay varias opciones cuando se trabaja con AFNetworking, pero para que nuestra aplicación esté preparada para el futuro, optaremos por la clase AFHTTPClient. Esta clase está diseñada para consumir servicios web, como la API de Forecast. Aunque solo accederemos a un punto final de la API, sigue siendo útil hacer uso del AFHTTPClient, como aprenderás en unos momentos.
Se recomienda crear una subclase AFHTTPClient para cada servicio web. Debido a que ya agregamos AFNetworking a nuestro proyecto en el tutorial anterior, podemos comenzar inmediatamente a subclasificar AFHTTPClient.
Paso 1: Crear la clase
Crea una nueva clase Objective-C, asígnale el nombre MTForecastClient y conviértela en una subclase de AFHTTPClient (figura 2).



Paso 2: Creación de un objeto Singleton
Adoptaremos el patrón singleton para facilitar el uso de la clase MTForecastClient en nuestro proyecto. Esto significa que solo una instancia de la clase está activa a la vez durante la vida útil de la aplicación. Lo más probable es que ya estés familiarizado con el patrón singleton, ya que es un patrón común en muchos lenguajes de programación orientados a objetos. A primera vista, el patrón singleton parece muy conveniente, pero hay una serie de advertencias a tener en cuenta. Puedes obtener más información sobre los singleton leyendo este excelente artículo de Matt Gallagher.
Crear un objeto singleton es bastante sencillo en Objective-C. Comienza declarando un método de clase en MTForecastClient.h para proporcionar acceso público al objeto singleton (ver más abajo).
1 |
|
2 |
#import "AFHTTPClient.h"
|
3 |
|
4 |
@interface MTForecastClient : AFHTTPClient |
5 |
|
6 |
#pragma mark -
|
7 |
#pragma mark Shared Client
|
8 |
+ (MTForecastClient *)sharedClient; |
9 |
|
10 |
@end
|
La implementación de sharedClient puede parecer desalentadora al principio, pero no es tan difícil una vez que comprendes lo que está sucediendo. Primero declaramos dos variables estáticas, (1) predicate de tipo dispatch_once_t y (2) _sharedClient de tipo MTForecastClient. Como su nombre lo indica, predicate es un predicado que usamos en combinación con la función dispatch_once. Cuando se trabaja con una variable de tipo dispatch_once_t, es importante que se declare estáticamente. La segunda variable, _sharedClient, almacenará una referencia al objeto singleton.
La función dispatch_once toma un puntero a una estructura dispatch_once_t, el predicado y un bloque. La belleza de dispatch_once es que ejecutará el bloque una vez durante la vida útil de la aplicación, lo cual es exactamente lo que queremos. La función dispatch_once no tiene muchos usos, pero este es definitivamente uno de ellos. En el bloque que pasamos a dispatch_once, creamos el objeto singleton y almacenamos una referencia en _sharedClient. Es más seguro invocar alloc e init por separado para evitar una condición de carrera que podría conducir a un punto muerto. ¡Espera!... ¿qué? Puedes leer más sobre los detalles esenciales en Stack Overflow.
1 |
|
2 |
+ (MTForecastClient *)sharedClient { |
3 |
static dispatch_once_t predicate; |
4 |
static MTForecastClient *_sharedClient = nil; |
5 |
|
6 |
dispatch_once(&predicate, ^{ |
7 |
_sharedClient = [self alloc]; |
8 |
_sharedClient = [_sharedClient initWithBaseURL:[self baseURL]]; |
9 |
});
|
10 |
|
11 |
return _sharedClient; |
12 |
}
|
Lo importante que hay que entender sobre la implementación del método de clase sharedClient es que el inicializador, initWithBaseURL:, se invoca solo una vez. El objeto singleton se almacena en la variable estática _sharedClient, que es devuelta por el método de clase sharedClient.
Paso 3: Configurar el cliente
En sharedClient, invocamos initWithBaseURL:, que a su vez invoca baseURL, otro método de clase. En initWithBaseURL:, establecemos un encabezado predeterminado, lo que significa que el cliente agrega este encabezado a cada solicitud que envía. Ésta es una de las ventajas de trabajar con la clase AFHTTPClient. En initWithBaseURL:, también registramos una clase de operación HTTP invocando registerHTTPOperationClass:. La biblioteca AFNetworking proporciona una serie de clases de operaciones especializadas. Una de estas clases es la clase AFJSONRequestOperation, que facilita la interacción con una API JSON. Debido a que la API Forecast devuelve una respuesta JSON, la clase AFJSONRequestOperation es una buena opción. El método registerHTTPOperationClass: funciona de manera similar a cómo funciona registerClass:forCellReuseIdentifier: de la clase UITableView. Al decirle al cliente qué clase de operación queremos usar para interactuar con el servicio web, creará instancias de esa clase bajo el capó. El por qué esto es útil se aclarará en unos momentos.
1 |
|
2 |
- (id)initWithBaseURL:(NSURL *)url { |
3 |
self = [super initWithBaseURL:url]; |
4 |
|
5 |
if (self) { |
6 |
// Accept HTTP Header
|
7 |
[self setDefaultHeader:@"Accept" value:@"application/json"]; |
8 |
|
9 |
// Register HTTP Operation Class
|
10 |
[self registerHTTPOperationClass:[AFJSONRequestOperation class]]; |
11 |
}
|
12 |
|
13 |
return self; |
14 |
}
|
La implementación de baseURL no es más que un método conveniente para construir la URL base del cliente. La URL base es la URL que utiliza el cliente para acceder al servicio web. Es la URL sin ningún nombre de método ni parámetro. La URL base de la API de Forecast es https://api.forecast.io/forecast/. La clave de la API es parte de la URL como puedes ver. Esto puede parecer inseguro y en realidad lo es. No es difícil para alguien tomar la clave API, por lo que es aconsejable trabajar con un proxy para enmascarar la clave API. Debido a que este enfoque es un poco más complicado, no cubriré este aspecto en esta serie.
1 |
|
2 |
+ (NSURL *)baseURL { |
3 |
return [NSURL URLWithString:[NSString stringWithFormat:@"https://api.forecast.io/forecast/%@/", MTForecastAPIKey]]; |
4 |
}
|
Es posible que hayas notado en la implementación de baseURL que he usado otra constante de cadena para almacenar la clave API. Esto puede parecer innecesario ya que solo usamos la clave API en una ubicación. Sin embargo, es una buena práctica almacenar los datos de la aplicación en una ubicación o en una lista de propiedades.
1 |
|
2 |
#pragma mark -
|
3 |
#pragma mark Forecast API
|
4 |
extern NSString * const MTForecastAPIKey; |
1 |
|
2 |
#pragma mark -
|
3 |
#pragma mark Forecast API
|
4 |
NSString * const MTForecastAPIKey = @"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; |
Paso 4: Agregando un método auxiliar
Antes de continuar, me gustaría extender la clase MTForecastClient agregando un método auxiliar o conveniente que facilitará la consulta de la API de Forecast. Este método de conveniencia aceptará una ubicación y un bloque de finalización. El bloque de finalización se ejecuta cuando se finaliza la solicitud. Para facilitar el trabajo con bloques, se recomienda declarar un tipo de bloque personalizado como se muestra a continuación. Si todavía te sientes incómodo usando bloques, te recomiendo leer este gran artículo de Akiel Khan.
El bloque toma dos argumentos, (1) un booleano que indica si la consulta fue exitosa y (2) un diccionario con la respuesta de la consulta. El método de conveniencia, requestWeatherForCoordinate:completion:, toma las coordenadas de una ubicación (CLLocationCoordinate2D) y un bloque de finalización. Al usar un bloque de finalización, podemos evitar la creación de un protocolo de delegado personalizado o recurrir al uso de notificaciones. Los bloques encajan perfectamente en este tipo de escenario.
1 |
|
2 |
#import "AFHTTPClient.h"
|
3 |
|
4 |
typedef void (^MTForecastClientCompletionBlock)(BOOL success, NSDictionary *response); |
5 |
|
6 |
@interface MTForecastClient : AFHTTPClient |
7 |
|
8 |
#pragma mark -
|
9 |
#pragma mark Shared Client
|
10 |
+ (MTForecastClient *)sharedClient; |
11 |
|
12 |
#pragma mark -
|
13 |
#pragma mark Instance Methods
|
14 |
- (void)requestWeatherForCoordinate:(CLLocationCoordinate2D)coordinate completion:(MTForecastClientCompletionBlock)completion; |
15 |
|
16 |
@end
|
En requestWeatherForCoordinate:completion:, invocamos getPath:success:failure:, un método declarado en AFHTTPClient. El primer argumento es la ruta que se adjunta a la URL base que creamos anteriormente. El segundo y tercer argumento son bloques que se ejecutan cuando la solicitud tiene éxito y falla, respectivamente. Los bloques de éxito y fracaso son bastante simples. Si se pasó un bloque de finalización a requestWeatherForCoordinate:completion:, ejecutamos el bloque y pasamos un valor booleano y el diccionario de respuesta (o nil en el bloque de fallas). En el bloque de fallas, registramos el error del bloque de fallas en la consola para facilitar la depuración.
1 |
|
2 |
- (void)requestWeatherForCoordinate:(CLLocationCoordinate2D)coordinate completion:(MTForecastClientCompletionBlock)completion { |
3 |
NSString *path = [NSString stringWithFormat:@"%f,%f", coordinate.latitude, coordinate.longitude]; |
4 |
[self getPath:path parameters:nil success:^(AFHTTPRequestOperation *operation, id response) { |
5 |
if (completion) { |
6 |
completion(YES, response); |
7 |
}
|
8 |
|
9 |
} failure:^(AFHTTPRequestOperation *operation, NSError *error) { |
10 |
if (completion) { |
11 |
completion(NO, nil); |
12 |
|
13 |
NSLog(@"Unable to fetch weather data due to error %@ with user info %@.", error, error.userInfo); |
14 |
}
|
15 |
}];
|
16 |
}
|
Quizás te estés preguntando cuál es el objeto response en los bloques de éxito o las referencias. Aunque la API de Forecast devuelve una respuesta de tipo JSON, el objeto response en el bloque de éxito es una instancia de NSDictionary. El beneficio de trabajar con la clase AFJSONHTTPRequestOperation, que registramos en initWithBaseURL:, es que acepta la respuesta JSON y crea automáticamente un objeto a partir de los datos de esa respuesta, un diccionario en este ejemplo.
2. Consultando la API de Forecast
Paso 1: Modificar el setLocation:
Armado con la clase MTForecastClient, es hora de consultar la API de Forecast y obtener los datos meteorológicos para la ubicación actualmente seleccionada. El lugar más adecuado para hacer esto es en el método setLocation: de la clase MTWeatherViewController. Modifica el método setLocation: como se muestra a continuación. Como puedes ver, todo lo que hacemos es invocar a fetchWeatherData, otro método auxiliar.
1 |
|
2 |
- (void)setLocation:(NSDictionary *)location { |
3 |
if (_location != location) { |
4 |
_location = location; |
5 |
|
6 |
// Update User Defaults
|
7 |
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; |
8 |
[ud setObject:location forKey:MTRainUserDefaultsLocation]; |
9 |
[ud synchronize]; |
10 |
|
11 |
// Post Notification
|
12 |
NSNotification *notification1 = [NSNotification notificationWithName:MTRainLocationDidChangeNotification object:self userInfo:location]; |
13 |
[[NSNotificationCenter defaultCenter] postNotification:notification1]; |
14 |
|
15 |
// Update View
|
16 |
[self updateView]; |
17 |
|
18 |
// Request Location
|
19 |
[self fetchWeatherData]; |
20 |
}
|
21 |
}
|
¿Alguna vez te has preguntado por qué utilizo tantos métodos auxiliares en mi código? La razón es simple. Al incluir la funcionalidad en métodos de ayuda, es muy fácil reutilizar el código en varios lugares de un proyecto. El principal beneficio, sin embargo, es que ayuda a combatir la duplicación de código. La duplicación de código es algo que siempre debes intentar evitar tanto como sea posible. Otra ventaja de usar métodos auxiliares es que hace que tu código sea mucho más legible. Al crear métodos que hacen una cosa y proporcionar un nombre de método bien elegido, es más fácil leer y procesar tu código rápidamente.
Paso 2: Enviando la solicitud
Es hora de poner en uso la biblioteca SVProgressHUD. Realmente me gusta esta biblioteca porque es muy simple de usar sin saturar la base de código del proyecto. Echa un vistazo a la implementación de fetchWeatherData a continuación. Comenzamos mostrando el HUD de progreso y luego pasamos una estructura (CLLocationCoordinate2D) al método de conveniencia que creamos anteriormente, requestWeatherForCoordinate:completion:. En el bloque de finalización, ocultamos el HUD de progreso y registramos la respuesta en la consola.
1 |
|
2 |
- (void)fetchWeatherData { |
3 |
// Show Progress HUD
|
4 |
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient]; |
5 |
|
6 |
// Query Forecast API
|
7 |
double lat = [[_location objectForKey:MTLocationKeyLatitude] doubleValue]; |
8 |
double lng = [[_location objectForKey:MTLocationKeyLongitude] doubleValue]; |
9 |
[[MTForecastClient sharedClient] requestWeatherForCoordinate:CLLocationCoordinate2DMake(lat, lng) completion:^(BOOL success, NSDictionary *response) { |
10 |
// Dismiss Progress HUD
|
11 |
[SVProgressHUD dismiss]; |
12 |
|
13 |
NSLog(@"Response > %@", response); |
14 |
}];
|
15 |
}
|
Antes de compilar y ejecutar tu aplicación, importa el archivo de encabezado de la clase MTForecastClient en MTWeatherViewController.m.
1 |
|
2 |
#import "MTWeatherViewController.h"
|
3 |
|
4 |
#import "MTForecastClient.h"
|
5 |
|
6 |
@interface MTWeatherViewController () <CLLocationManagerDelegate> { |
7 |
BOOL _locationFound; |
8 |
}
|
9 |
|
10 |
@property (strong, nonatomic) NSDictionary *location; |
11 |
|
12 |
@property (strong, nonatomic) CLLocationManager *locationManager; |
13 |
|
14 |
@end
|
¿Qué sucede cuando el dispositivo no está conectado a la web? ¿Has pensado en ese escenario? En términos de experiencia del usuario, es una buena práctica notificar al usuario cuando la aplicación no puede solicitar datos de la API de pronóstico. Déjame mostrarte cómo hacer esto con la biblioteca AFNetworking.
3. Accesibilidad
Hay una serie de bibliotecas que proporcionan esta funcionalidad, pero nos quedaremos con AFNetworking. Apple también proporciona código de muestra, pero está un poco desactualizado y no es compatible con ARC.
AFNetworking realmente ha adoptado los bloques, que es definitivamente una de las razones por las que esta biblioteca se ha vuelto tan popular. Monitorear los cambios de accesibilidad es tan simple como pasar un bloque a setReachabilityStatusChangeBlock:, otro método de la clase AFHTTPClient. El bloque se ejecuta cada vez que se cambia el estado de accesibilidad y acepta un único argumento de tipo AFNetworkReachabilityStatus. Echa un vistazo al método actualizado llamado initWithBaseURL: de la clase MTForecastClient.
1 |
|
2 |
- (id)initWithBaseURL:(NSURL *)url { |
3 |
self = [super initWithBaseURL:url]; |
4 |
|
5 |
if (self) { |
6 |
// Accept HTTP Header
|
7 |
[self setDefaultHeader:@"Accept" value:@"application/json"]; |
8 |
|
9 |
// Register HTTP Operation Class
|
10 |
[self registerHTTPOperationClass:[AFJSONRequestOperation class]]; |
11 |
|
12 |
// Reachability
|
13 |
__weak typeof(self)weakSelf = self; |
14 |
[self setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { |
15 |
[[NSNotificationCenter defaultCenter] postNotificationName:MTRainReachabilityStatusDidChangeNotification object:weakSelf]; |
16 |
}];
|
17 |
}
|
18 |
|
19 |
return self; |
20 |
}
|
Para evitar un ciclo de retención, pasamos una referencia débil al objeto singleton en el bloque que pasamos a setReachabilityStatusChangeBlock:. Incluso si usas ARC en tus proyectos, aún debes estar al tanto de problemas de memoria sutiles como este. El nombre de la notificación que publicamos es otra constante de cadena declarada en MTConstants.h/.m.
1 |
|
2 |
extern NSString * const MTRainReachabilityStatusDidChangeNotification; |
1 |
|
2 |
NSString * const MTRainReachabilityStatusDidChangeNotification = @"com.mobileTuts.MTRainReachabilityStatusDidChangeNotification"; |
La razón para publicar una notificación en el bloque de cambio de estado de accesibilidad es para facilitar la actualización de otras partes de la aplicación cuando cambia la accesibilidad del dispositivo. Para asegurarte de que se notifique a la clase MTWeatherViewController de los cambios de accesibilidad, se agregan instancias de la clase como observador de las notificaciones enviadas por el cliente de Forecast como se muestra a continuación.
1 |
|
2 |
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { |
3 |
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; |
4 |
|
5 |
if (self) { |
6 |
// Initialize Location Manager
|
7 |
self.locationManager = [[CLLocationManager alloc] init]; |
8 |
|
9 |
// Configure Location Manager
|
10 |
[self.locationManager setDelegate:self]; |
11 |
[self.locationManager setDesiredAccuracy:kCLLocationAccuracyKilometer]; |
12 |
|
13 |
// Add Observer
|
14 |
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; |
15 |
[nc addObserver:self selector:@selector(reachabilityStatusDidChange:) name:MTRainReachabilityStatusDidChangeNotification object:nil]; |
16 |
}
|
17 |
|
18 |
return self; |
19 |
}
|
Esto también significa que debemos eliminar la instancia como observador en el método dealloc. Este es un detalle que a menudo se pasa por alto.
1 |
|
2 |
- (void)dealloc { |
3 |
// Remove Observer
|
4 |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
5 |
}
|
La implementación de reachabilityStatusDidChange: es bastante básica en este momento. Actualizaremos su implementación una vez que creemos la interfaz de usuario de la aplicación.
1 |
|
2 |
- (void)reachabilityStatusDidChange:(NSNotification *)notification { |
3 |
MTForecastClient *forecastClient = [notification object]; |
4 |
NSLog(@"Reachability Status > %i", forecastClient.networkReachabilityStatus); |
5 |
}
|
4. Actualización de datos
Antes de terminar esta publicación, quiero agregar dos funciones adicionales, (1) obtener datos meteorológicos cada vez que la aplicación se activa y (2) agregar la capacidad de actualizar manualmente los datos meteorológicos. Podríamos implementar un temporizador que obtenga datos nuevos aproximadamente cada hora, pero en mi opinión, esto no es necesario para una aplicación meteorológica. La mayoría de los usuarios iniciarán la aplicación, verán el clima y colocarán la aplicación en segundo plano. Por lo tanto, solo es necesario obtener datos nuevos cuando el usuario inicia la aplicación. Esto significa que debemos escuchar las notificaciones UIApplicationDidBecomeActiveNotification en la clase MTWeatherViewController. Como hicimos para monitorear los cambios de accesibilidad, agregamos instancias de la clase como observadores de notificaciones de tipo UIApplicationDidBecomeActiveNotification.
1 |
|
2 |
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { |
3 |
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; |
4 |
|
5 |
if (self) { |
6 |
// Initialize Location Manager
|
7 |
self.locationManager = [[CLLocationManager alloc] init]; |
8 |
|
9 |
// Configure Location Manager
|
10 |
[self.locationManager setDelegate:self]; |
11 |
[self.locationManager setDesiredAccuracy:kCLLocationAccuracyKilometer]; |
12 |
|
13 |
// Add Observer
|
14 |
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; |
15 |
[nc addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; |
16 |
[nc addObserver:self selector:@selector(reachabilityStatusDidChange:) name:MTRainReachabilityStatusDidChangeNotification object:nil]; |
17 |
}
|
18 |
|
19 |
return self; |
20 |
}
|
En applicationDidBecomeActive:, verificamos que la ubicación esté configurada (no en nil) porque esto no siempre será cierto. Si la ubicación es válida, obtenemos los datos meteorológicos.
1 |
|
2 |
- (void)applicationDidBecomeActive:(NSNotification *)notification { |
3 |
if (self.location) { |
4 |
[self fetchWeatherData]; |
5 |
}
|
6 |
}
|
También modifiqué fetchWeatherData para consultar solo la API de pronóstico si el dispositivo está conectado a la web.
1 |
|
2 |
- (void)fetchWeatherData { |
3 |
if ([[MTForecastClient sharedClient] networkReachabilityStatus] == AFNetworkReachabilityStatusNotReachable) return; |
4 |
|
5 |
// Show Progress HUD
|
6 |
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient]; |
7 |
|
8 |
// Query Forecast API
|
9 |
double lat = [[_location objectForKey:MTLocationKeyLatitude] doubleValue]; |
10 |
double lng = [[_location objectForKey:MTLocationKeyLongitude] doubleValue]; |
11 |
[[MTForecastClient sharedClient] requestWeatherForCoordinate:CLLocationCoordinate2DMake(lat, lng) completion:^(BOOL success, NSDictionary *response) { |
12 |
// Dismiss Progress HUD
|
13 |
[SVProgressHUD dismiss]; |
14 |
|
15 |
// NSLog(@"Response > %@", response);
|
16 |
}];
|
17 |
}
|
Agreguemos un botón al controlador de vista meteorológica que el usuario puede tocar para actualizar manualmente los datos meteorológicos. Crea una salida en MTWeatherViewController.h y crea una acción de tipo refresh: en MTWeatherViewController.m.
1 |
|
2 |
#import <UIKit/UIKit.h>
|
3 |
|
4 |
#import "MTLocationsViewController.h"
|
5 |
|
6 |
@interface MTWeatherViewController : UIViewController <MTLocationsViewControllerDelegate> |
7 |
|
8 |
@property (weak, nonatomic) IBOutlet UILabel *labelLocation; |
9 |
@property (weak, nonatomic) IBOutlet UIButton *buttonRefresh; |
10 |
|
11 |
@end
|
1 |
|
2 |
- (IBAction)refresh:(id)sender { |
3 |
if (self.location) { |
4 |
[self fetchWeatherData]; |
5 |
}
|
6 |
}
|
Abre MTWeatherViewController.xib, agrega un botón a la vista del controlador de la vista con un título de Actualizar y conecta la salida y la acción con el botón (figura 3). La razón para crear una salida para el botón es poder deshabilitarlo cuando no hay conexión de red disponible. Para que esto funcione, necesitamos actualizar el método reachabilityStatusDidChange: como se muestra a continuación.



1 |
|
2 |
- (void)reachabilityStatusDidChange:(NSNotification *)notification { |
3 |
MTForecastClient *forecastClient = [notification object]; |
4 |
NSLog(@"Reachability Status > %i", forecastClient.networkReachabilityStatus); |
5 |
|
6 |
// Update Refresh Button
|
7 |
self.buttonRefresh.enabled = (forecastClient.networkReachabilityStatus != AFNetworkReachabilityStatusNotReachable); |
8 |
}
|
No es necesario desactivar temporalmente el botón de actualización cuando se procesa una solicitud en fetchWeatherData porque el HUD de progreso agrega una capa en la parte superior de la vista del controlador de vista que evita que el usuario toque el botón más de una vez. Compila y ejecuta la aplicación para probar todo.
Bono: Eliminar ubicaciones
Un lector me preguntó cómo eliminar ubicaciones de la lista, por lo que la incluyo aquí en aras de la integridad. Lo primero que debemos hacer es decirle a la vista de tabla qué filas son editables implementando tableView:canEditRowAtIndexPath: del protocolo UITableViewDataSource. Este método devuelve YES si la fila en indexPath es editable y NO si no lo es. La implementación es simple como puedes ver a continuación. Cada fila es editable excepto la primera fila y la ubicación seleccionada actualmente.
1 |
|
2 |
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { |
3 |
if (indexPath.row == 0) { |
4 |
return NO; |
5 |
}
|
6 |
|
7 |
// Fetch Location
|
8 |
NSDictionary *location = [self.locations objectAtIndex:(indexPath.row - 1)]; |
9 |
|
10 |
return ![self isCurrentLocation:location]; |
11 |
}
|
Para comprobar si location es la ubicación actual, utilizamos otro método auxiliar, isCurrentLocation:, en el que buscamos la ubicación actual y comparamos las coordenadas de las ubicaciones. Hubiera sido mejor (y más fácil) si hubiéramos asignado un identificador único a cada ubicación almacenada en la base de datos de valores predeterminados del usuario. No solo facilitaría la comparación de ubicaciones, sino que también nos permitiría almacenar el identificador único de la ubicación actual en la base de datos de valores predeterminados del usuario y buscarlo en la matriz de ubicaciones. El problema con la implementación actual es que las ubicaciones con las mismas coordenadas exactas no se pueden distinguir entre sí.
1 |
|
2 |
- (BOOL)isCurrentLocation:(NSDictionary *)location { |
3 |
// Fetch Current Location
|
4 |
NSDictionary *currentLocation = [[NSUserDefaults standardUserDefaults] objectForKey:MTRainUserDefaultsLocation]; |
5 |
|
6 |
if ([location[MTLocationKeyLatitude] doubleValue] == [currentLocation[MTLocationKeyLatitude] doubleValue] && |
7 |
[location[MTLocationKeyLongitude] doubleValue] == [currentLocation[MTLocationKeyLongitude] doubleValue]) { |
8 |
return YES; |
9 |
}
|
10 |
|
11 |
return NO; |
12 |
}
|
Cuando el usuario toca el botón eliminar de una fila de vista de tabla, la fuente de datos de la vista de tabla recibe un mensaje tableView:commitEditingStyle:forRowAtIndexPath:. En este método, necesitamos (1) actualizar la fuente de datos, (2) guardar los cambios en la base de datos de valores predeterminados del usuario y (3) actualizar la vista de tabla. Si editStyle es igual a UITableViewCellEditingStyleDelete, eliminamos la ubicación de la matriz locations y almacenamos la matriz actualizada en la base de datos de valores predeterminados del usuario. También eliminamos la fila de la vista de tabla para reflejar el cambio en la fuente de datos.
1 |
|
2 |
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { |
3 |
if (editingStyle == UITableViewCellEditingStyleDelete) { |
4 |
// Update Locations
|
5 |
[self.locations removeObjectAtIndex:(indexPath.row - 1)]; |
6 |
|
7 |
// Update User Defaults
|
8 |
[[NSUserDefaults standardUserDefaults] setObject:self.locations forKey:MTRainUserDefaultsLocations]; |
9 |
|
10 |
// Update Table View
|
11 |
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop]; |
12 |
}
|
13 |
}
|
Para alternar el estilo de edición de la vista de tabla, necesitamos agregar un botón de edición a la interfaz de usuario. Crea una salida para el botón en MTLocationsViewController.h y una acción denominada editLocations: en MTLocationsViewController.m. En editLocations:, cambiamos el estilo de edición de la vista de tabla.
1 |
|
2 |
#import <UIKit/UIKit.h>
|
3 |
|
4 |
@protocol MTLocationsViewControllerDelegate; |
5 |
|
6 |
@interface MTLocationsViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> |
7 |
|
8 |
@property (weak, nonatomic) id<MTLocationsViewControllerDelegate> delegate; |
9 |
|
10 |
@property (weak, nonatomic) IBOutlet UITableView *tableView; |
11 |
@property (weak, nonatomic) IBOutlet UIBarButtonItem *editButton; |
12 |
|
13 |
@end
|
14 |
|
15 |
@protocol MTLocationsViewControllerDelegate <NSObject> |
16 |
- (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller; |
17 |
- (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location; |
18 |
@end
|
1 |
|
2 |
- (IBAction)editLocations:(id)sender { |
3 |
[self.tableView setEditing:![self.tableView isEditing] animated:YES]; |
4 |
}
|
Abre MTLocationsViewController.xib, agrega una barra de navegación a la vista del controlador de vista y agrega un botón de edición a la barra de navegación. Conecta el botón de edición con la salida y la acción que creamos hace un momento.



Quizás te preguntes por qué creamos una salida para el botón de edición. La razón es que necesitamos poder cambiar el título del botón de edición de Editar a Listo, y viceversa, siempre que cambie el estilo de edición de la vista de tabla. Además, cuando el usuario elimina la última ubicación (excepto la ubicación actual) en la vista de tabla, sería bueno alternar automáticamente el estilo de edición de la vista de tabla. Estas funciones no son difíciles de implementar, por lo que te las dejo a ti como ejercicio. Si tienes problemas o tienes preguntas, no dudes en dejar un comentario debajo de este artículo.
Conclusión
Hemos integrado con éxito la API de Forecast en nuestra aplicación meteorológica. En el siguiente tutorial, implementaremos el enfoque en la interfaz de usuario y el diseño de la aplicación.



