Geolocalización y multitarea en iOS: posicionamiento GPS y MapKit de Google

Geolocalización y multitarea en iOS: posicionamiento GPS y MapKit de Google
En este capítulo de programación del Curso de Aplicaciones de iOS me gustaría hablar sobre la localización en dispositivos que usan iOS así como de la multitarea ya que son cosas muy relacionadas, por eso remataremos el tema con los avisos, es decir, notificaciones locales, esos mensajitos que los programas lanzan, y como poner un número en nuestro icono de nuestra aplicación, también llamado «badge»; que por cierto también lo tiene la clase UITabBarItem, o sea, un botón de una barra de botones tipo pestañas o tab’s… ;-)

Multitarea en las aplicaciones de iOS

Las plantillas generadas por el IDE XCode 4 ya traen las funciones que toda aplicación debe utilizar cuando la aplicación pasa entre los diferentes estados que iOS le permite. Cuando usamos el botón Home desde nuestra aplicación hay un evento, otro cuando este ha terminado, otro cuando se vuelve a la aplicación, etc.

Debemos utilizar la menor cantidad de recursos posible durante la multitarea ya que si nos pasamos el SO decidirá cerrar nuestra app aunque la mayoría de apps no ejecutan ningún código durante su tiempo en modo de segundo plano.

Esta multitarea, como sabemos está disponible sólo para dispositivos con firmware a partir de la versión 4, que fué cuando se introdujo dicha característica, para ello existe la función del SDK multitaskingSupported que nos informará de cada caso.

 
UIDevice* device = [UIDevice currentDevice];
BOOL backgroundSupported = NO;
if ([device respondsToSelector:@selector(isMultitaskingSupported)])
backgroundSupported = device.multitaskingSupported;

Lo primero que debemos hacer es incluir la opción UIBackgroundModes en nuestro Info.plist, y se debe especificar los valores: audio, location y/o voip para disponer de estas características durante el trabajo en segundo plano.

En nuestra clase Delegate de la aplicación tenemos las funciones siguientes:

  • applicationdidFinishLaunchingWithOptions
  • applicationDidBecomeActive
  • applicationWillResignActive
  • applicationDidEnterBackground
  • applicationWillEnterForeground
  • applicationWillTerminate

No se recomienda utilizar llamadas a OpenGL (tampoco sería muy lógico) durante el segundo plano, debemos cancelar todos los servicios Bonjour antes de pasar a este estado así como no mostrar mensajes si hay errores de conexión, guardar datos, etc. Si tenemos una aplicación que muestra muchos datos visuales lo mejor es liberarlos hasta que se vuelva del segundo plano, pero sí debemos responder a las notificaciones de conexión y desconexión.

Para realizar la inclusión de una tarea en segundo plano existe la función llamada beginBackgroundTaskWithExpirationHandler, que pide al sistema un tiempo extra para completar una tarea larga y para finalizarla tenemos endBackgroundTask, sabemos el tiempo que lleva ejecutándose gracias a la propiedad backgroundTimeRemaining de nuestra UIApplication. Deberíamos usar estas funciones y propiedades si estamos descargando archivos de datos, configurando o guardando información sensible.

Un ejemplo:

 
-(void)applicationDidEnterBackground:(UIApplication *)
application {
UIApplication* app = [UIApplication sharedApplication];
 
//Pedir permiso para ejecutar en background. Proveer de un manejador por
//si la tarea requiere más tiempo
 
NSAssert (bgTask == UIBackgroundTaskInvalid, nil);
 
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
dispactch_async(dispatch_get_main_queue(), ^{
 
if (bgTask != UIBackgroundTaskInvalid) {
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
})
}];
 
dispatch_async(dispatch_get_global_queue(
    DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
if (bgTask != UIBackgroundTaskInvalid) {
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
});
});
}

Notificaciones:

No son más que tareas en segundo plano pero que usan la clase UILocalNotification, los tipos existentes son: sound (sonido como los de whatsapp o un sms),alert (el texto de un sms, etc.)  y badge (el número que hay sobre el icono de la app).

Se pueden programar hasta 128 notificaciones simultáneas, cada una de ellas se puede repetir en un intervalo prefijado por el programador.

Veamos un ejemplo de una alarma con un archivo de sonido donde se comprueba si está en segundo plano:

 
- (void) scheduleAlarmForDate: (NSDate *) theDate {
UIApplication* app = [UIApplication sharedApplication];
NSArray* oldNotifications = 
 [app scheduledLocalNotifications];
 
if ([oldNotifications count] > 0)
[app cancelAllLocalNotifications];
 
UILocalNotification* alarm = [[
  [UILocalNotification alloc] init] autorelease];
 
if (alarm) {
alarm.fireDate = theDate;
alarm.timeZone = [NSTimeZone defaultTimeZone];
alarm.repeatInterval = 0;
alarm.soundName = @"alarmsound.caf";
alarm.alertBody = @"Time to wake up!";
 
[app scheduleLocalNotification:alarm];
}
}

Para el audio se indica en nuestro Info.plist, una cadena «audio» en UIBackgroundModes como hemos visto, cuando la aplicación esté en segundo plano ,limitaremos que se ejecute más de lo necesario…En el caso de «voip» se debe configurar además un socket, por medio de la función setKeepAliveTimeout: handler: para especificar la frecuencia con la que despertar la aplicación para el correcto funcionamiento del servicio.

Para las notificaciones por posicionamiento, sólo se realizan con cambios significativos de localización, aunque pueden seguir usando los servicios de localización en segundo plano no conviene abusar a menos que desarrollemos un GPS como Tomtom.

Activación:

 
CLLocationManager *locManager = [[[CLLocationManager] alloc] init ];
[locManager startMonitoringSignificantLocationChanges];

Podemos además usar la característica de despertar y relanzar nuestra aplicación si nos llega una notificación push.

Location Framework

El posicionamiento es posible usarlo a través del SDK de Apple con un iDevice gracias al Location Framework, que nos proporciona la localización del usuario, información que podemos utilizar para mostrar recursos cercanos, orientar en rutas, realizar un seguimiento o tracking del desplazamiento, dibujar zonas o áreas sobre el mapa con líneas y otras primitivas geométricas, etc. Además el Location Framework da acceso a la brújula para mejorar la experiencia a la hora de realizar una orientación más realista, por ejemplo se usa en la aplicación Mapas de Google al pulsar el icono de brújula

Configurando el método de posicionamiento

iOS nos permite conseguir la localización actual con diferentes métodos de posicionamiento:

  • Por cambio significativo: es un método para bajo consumo de batería
  • Servicios de posicionamiento estándar: es más configurable
  • Monitorización por región: para registrar cambios en una zona definida

Para aplicaciones que utilicen CoreLocation.framework ( #import <CoreLocation/CoreLocation.h>) además debemos añadir al Info.plist (recordar cómo se configura una aplicación en la anterior entrega de este curso de apps) de nuestra aplicación el campo UIRequiredDeviceCapabilities y la llave «location-services» si sólo necesitamos posicionamiento general (aproximado, por ejemplo con una triangulación por antenas de telefonía bastaría) o bien especificamos además la llave «gps» para usar el hardware del iPhone y el iPad (1,2,etc).

Una vez seleccionado el método y configurada la aplicación, pasamos a inicializar el servicio de posicionamiento, para ello utilizamos la clase CLLocationManager,

Obtener la posición del usuario (por defecto)

Nada mejor que un ejemplo para mostrar el funcionamiento:

- (void)startStandardUpdates
{
// Crea el objeto location manager si no existe
if (nil == locationManager){
locationManager = [[CLLocationManager alloc] init];
    }
    //Especificamos la clase actual como clase que maneje los
    // eventos del localizador
locationManager.delegate = self; 
//la clase actual debe heredar de CLLocationManagerDelegate
    //Ahora especificamos una precisión
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
 
// para los movimientos entre eventos que suceden:
locationManager.distanceFilter = 500;
    //inicializar ya!
[locationManager startUpdatingLocation];
}

Con desiredAccuracy (kCLLocationAccuracyBest) se establece la precisión del posicionamiento, con distanceFilter (kClDistanceFilterNode) se configura la distancia mínima que es necesario que se aprecie en un desplazamiento del dispositivo para que se produzca un evento de actualización de la localización actual del usuario.

Obtener la posición del usuario por cambio significarivo

- (void)startSignificantChangeUpdates
{
// Crear el objeto the location manager si no existe:
if (nil == locationManager){
locationManager = [[CLLocationManager alloc] init];
    }
locationManager.delegate = self; //igual que antes
    //esto es lo nuevo, inicialización con actualización por cambio significativo
[locationManager startMonitoring SignificantLocationChanges];
}

Crear una clase para las «chinchetas» o pins sobre el mapa, esta clase almacena información relevante sobre lo que queremos mostrar al pinchar sobre ella, desde el propio pin o chincheta (situación, color, imagen, animación,etc.) además del pequeño título y subtítulo que se muestra y la posterior acción a realizar al extender la información de la misma.

¿Qué es un «pin»?: Podemos extender la clase (por herencia) de una MkAnnotation y luego en la clase controlador de la vista (UIViewController) extender esta al delegado de un mapa (UIVIewController <MkMapViewDelegate>) para poder recoger el evento de cuando se pinta un pin o chincheta y decirle al objeto MkMapView cómo mostrar la información que almacena la clase anotación, es en este evento controlado donde cambiamos la imagen del pin asociado al MkAnnotation, el botón de acción o cualquier otra cosa que queramos a voluntad.

Como observación, recordad que cuando MKMapView carga podemos utilizar la posición del usuario que viene integrada como opción (UseUserLocation, el framework CoreLocation nos proporciona otra, debemos saber cuál utilizar en qué caso ya que no podemos tener siempre cargado un mapa, o sincronizar los valores de diferentes clases cada vez que la posición cambia, etc. Si ocurre un error al recibir la información de localización el evento locationManagerdidFailWithError nos informará de ello, a veces, el gestor de posicionamiento devuelve información en una caché así primero debemos comprobar en qué momento se generó mediante la propiedad timestamp de las posiciones recibidas.

Para consumir menos batería debemos desactivar los servicios de posicionamiento por medio de una configuración de usuario o bien cuando no se necesiten; usar el servicio de cambios significativos en lugar del servicio estándar, y un valor pequeño para distanceFilter (igual que en los videojuegos ,mientras mayor es el búfer, más memoria y procesamiento se necesita por lo que consume más batería). Por último, desactivar los eventos de posicionamiento si la precisión (diferencia entre cambios de posición) no mejora en un corto período de tiempo.

Información geolocalizada

Lo interesante de poder disponer de un framework de Google MapKit es que podemos extraer información a partir del campo longitud y latitud, un ejemplo que he utilizado en una de mis aplicaciones es el siguiente:

- (void) searchBarTextDidEndEditing:(UISearchBar *) _searchBar {
    [searchBar resignFirstResponder];
    NSError *error;
    NSString *urlString = [NSString stringWithFormat:
@"https://maps.google.es/maps/geo?q=%@&output=csv",
[_searchBar.text 
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    NSString *locationString = 
[NSString stringWithContentsOfURL:
 [NSURL URLWithString:urlString] 
encoding:NSISOLatin2StringEncoding 
error:&error];
    NSArray *listItems = [locationString 
componentsSeparatedByString:@","];
 
    if ([listItems count]&gt;=4 && 
[[listItems objectAtIndex:0] isEqualToString:@"200"]){
        if (birthLocation!=nil){
         [mk_mapView removeAnnotation:birthLocation];
         [birthLocation moveAnnotation:CLLocationCoordinate2DMake(
          [[listItems objectAtIndex:2] doubleValue], 
[[listItems objectAtIndex:3] doubleValue])];
            [birthLocation setTitle:_searchBar.text];
            [birthLocation setSubtitle:
[NSString stringWithFormat:@"%f,%f",
            [[listItems objectAtIndex:2] doubleValue],
            [[listItems objectAtIndex:3] doubleValue]]];
        } else {
         birthLocation = [[MapAnnotation alloc] initWithCoordinate:
            CLLocationCoordinate2DMake([
[listItems objectAtIndex:2] doubleValue],
 [[listItems objectAtIndex:3] doubleValue])
                title:_searchBar.text subtitle:
[NSString stringWithFormat:@"%f,%f",
                [[listItems objectAtIndex:2] doubleValue],
                [[listItems objectAtIndex:3] doubleValue]]
         ];
        }
        [mk_mapView addAnnotation:birthLocation];
    } else {
        [self AlertWithMessage:@"No se pudo encontrar la dirección"];
    }
 
}

Este código se aplica a una vista MkMapView con una barra de búsqueda, además he creado una clase para anotaciones básica MapAnnotation que lógicamente hereda de MkMapAnnotation (no instanciable a menos que se cree otra de NSObject que herede de esta).

Debemos guardar una pequeña caché de anotaciones para hacer más eficiente la aplicación, podemos utilizar el array de la propia vista del mapa de MkMapView.

En el caso en que queramos desarrollar un evento que nos vaya actualizando la información encontrada sobre una región haciendo una geolocalización inversa podemos crear una clase parecida a ésta:

@implementation MyGeocoderViewController (CustomGeocodingAdditions)
 
- (void)geocodeLocation:(CLLocation*)location forAnnotation:(MapLocation*)annotation {
MKReverseGeocoder* theGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:location.coordinate];
 
theGeocoder.delegate = self;
[theGeocoder start];
}
 
// Delegate methods
- (void)reverseGeocoder:(MKReverseGeocoder*)geocoder didFindPlacemark:(MKPlacemark*)place {
MapLocation* theAnnotation = [map annotationForCoordinate:place.coordinate];
 
if (!theAnnotation) return;
 
// Associate the placemark with the annotation.
theAnnotation.placemark = place;
 
// Add a More Info button to the annotation's view.
MKPinAnnotationView* view = (MKPinAnnotationView*)[map viewForAnnotation:annotation];
 
if (view && (view.rightCalloutAccessoryView == nil)) {
view.canShowCallout = YES;
view.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
}
}
 
- (void)reverseGeocoder:(MKReverseGeocoder*)geocoder didFailWithError:(NSError*)error {
NSLog(@"Could not retrieve the specified place information.\n");
}
@end
 
@implementation MKMapView (GeocoderAdditions)
- (MapLocation*)annotationForCoordinate:(CLLocationCoordinate2D)coord {
// Iterate through the map view's list of coordinates
// and return the first one whose coordinate matches
// the specified value exactly.
id theObj = nil;
 
for (id obj in [self annotations]) {
if (([obj isKindOfClass:[MapLocation class]])) {
 
MapLocation* anObj = (MapLocation*)obj;
 
if ((anObj.coordinate.latitude == coord.latitude) &&
(anObj.coordinate.longitude == coord.longitude))
{
theObj = anObj;
break;
}
}
}
 
return theObj;
}
@end

En el framework MapKit de Google se utiliza la proyección Mercator, que es un tipo específico de proyección cilíndrica para todo el globo.
Esto es muy útil para poder realizar una navegación fácil por las imágenes de un mapa tanto en longitud y latitud como en altura, esto se especifica con los tipos de datos para coordenadas que se especifican con CClocationCoordinate2D y las áreas con MKCoordinateSpan y MKCoordinateRegion.
Un punto del mapa es un par de valores (x, y) en la proyección Mercator. Se utilizan para simplificar los cálculos matemáticos entre puntos y áreas. Los puntos se especifican con MKMapPoint y las áreas con MKMapSize y MKMapRect. Un punto es una unidad gráfica asociada con el sistema de coordenadas de un objeto UIView. Por lo tanto, se puede asociar directamente la vista del mapa con la interfaz. Los puntos se especifican mediante CGPoint y las áreas mediante MKMapSize y MKMapRet.

Si queremos pasar al sistema de posicionamiento de un GPS estándar, podéis usar esta función de una de mis aplicaciones:

- (NSString*) toDegreesMinutesSeconds:(CLLocationCoordinate2D) 
 nLocation {
//Longitud: N, Latitud: W
int degreesW = nLocation.latitude;
double decimalW = fabs(nLocation.latitude - degreesW);
int minutesW = decimalW * 60;
int degreesN = nLocation.longitude;
double decimalN = fabs(nLocation.longitude - degreesN);
int minutesN = decimalN * 60;
return [NSString stringWithFormat:@"%dN%d;%dW%d",degreesW, 
 minutesW,degreesN,minutesN];
}

Por último sólo me queda recordaros los conceptos básicos de la geolocalización y posicionamiento con CoreLocation y MapKit de Google:

  • Se puede añadir un MapView mediante el Interface Builder.
    • De manera programática, se debe crear una instancia de MKMapView e inicializarlo mediante el método initWithFrame: y añadirlo como una subvista dentro de la jerarquía de vistas.
  • Propiedades principales de MapView
    • region (tipo MKCoordinateRegion).
    • Define la parte visible del mapa. Es posible cambiar esta propiedad en cualquier momento, asignando a ésta un valor.
    • centerCoordinate. Define la posición central del mapa
  • Anotaciones
    1) Definir un objeto de anotación concreto.
    • Es posible utilizar MKPointAnnotation para crear una anotación simple. Contiene un popup con un título y un subtítulo.
    • Definir un objeto personalizado que siga el protocolo MKAnnotation
    2) Definir una vista de anotación para presentar la información en la pantalla.
    • Si la anotación se puede representar con una imagen estática, se debe crear una instancia de MKAnnotationView y asignar la imagen a la propiedad image.
    • Si se desea crear la anotación de chincheta, crear una instancia de MKPinAnnotationView.
    • Si la imagen estática es insuficiente, se puede crear una subclase de MKAnnotationView e implementar el código de dibujado para representarla.
    3) Implementar el método mapView:viewForAnnotation: en el delegate del MapView.
    4) Añadir la anotación usando el método addAnnotation: o addAnnotations:.
  • Overlays
    • 1) Definir el Overlay apropiado (MKCircle, MKPolygon, MKPolyline o una subclase de MKShape o MKMultiPoint).
    • 2) Definir una vista para representar el overlay en la pantalla.
    • 3) Implementar el método mapView:viewForOverlay en el MapView delegate.
    • 4) Añadir el objeto al mapa mediante addOverlay:.

Y hasta aquí la entrega de este curso, los ejercicios de esta entrega son seguir el tutorial siguiente: introducción a MapKit.

« Ahora podéis volver al índice del Curso de aplicaciones de iOS

Artículos relacionados:

  1. samuel dice:

    buenas noches, estoy desarrollando una aplicación para una clase en la universidad, en la cual quiero encontrar una dirección ingresada por el usuario a trabes de un search bar y me la retorne en un mapa de google como el ejemplo que nos das con jaen, españa.
    este código lo e tratado de acoplar pero me dice que tiene errores, seria muy amable y me podrías indicar los pasos detallados a seguir para desarrollar tal sentencia, estaré atento a su respuesta muchísimas gracias mi correo es samuel.romero.suarez@gmail.com

  2. sara dice:

    podrias implementar este codigo en nuestra app?

    un saludo

  3. victor dice:

    Hola:
    Quiero desarrollar una aplicación que hace un tracking del desplazamiento.
    Como podria implementarlo?
    Gracias.

    • Juan Belón dice:

      Hola Víctor, mantén el modo GPS del dispositivo en modo de bajo consumo y cada cierto tiempo guardas (si quieres puedes refrescar a mayor precisión para mejorar la tasa de aciertos) la latitud y longitud en un array o en un fichero de caché, por ejemplo en una base de datos sqlite, luego pintas líneas de un punto a otro con la api de mapas que uses y listos

  4. Sergio dice:

    Hola, excelente tutorial!!. Me ha servido de mucho en mi proyecto. Ya que soy nuevo en objetive-c. quisiera agregar una pregunta, se podría detectar las coordenadas de otros iphone para saber la geolocalización?. Se trata de saber el lugar donde se encuentran los móviles de una empresa.
    Agradezco mucho si me podrias orientar sobre esta consulta.
    Muchas gracias

    • Juan Belón dice:

      Apple tiene software para eso, pero si quieres hacerlo tú por qué no haces que cada x tiempo cada iPhone envíe su localización a un servidor privado con JSON? y, que se descargue igual para saber dónde están los dispositivos colocando chinchetas en el mapa con sus datos…

      • Sergio dice:

        Muchas gracias por la pronta respuesta. Mi idea original era exactamente esa, pasar las coordenadas a una base de datos y actualizarla cada x tiempo. Luego leyendo un poco más empecé a suponer que habría una manera más directa para hacerlo, por eso la consulta. Mi proyecto se basa en (por ej.:) un cliente que quiere saber donde se encuentran los móviles más cercanos y marcar su trayectoria en un mapa al iniciar su marcha. Todo esto con usuarios registrados. Si puedes darme una noción sobre el tema de saber la ubicación de los móviles te agradezco, sino volveré a mi idea inicial que es como tu me dices al principio. Gracias nuevamente. Saludos

  5. Hola dice:

    Muchas gracias por compartir esto, Quisiera hacer algo similar para Android, sabes como podría hacerlo?

  6. joel dice:

    Hola:Quiero desarrollar una aplicación obtenga coordenadas GPS con un intervalo de tiempo asignado por el usuario , y que funcione aunque la applicacion este en estado inactivo o suspendido, tomando en cuenta el consumo de bateria para que no consuma demaciando.actualmente estoy usando el evento UIApplication.sharedApplication().setMinimumBackgroundFetchInterval( UIApplicationBackgroundFetchIntervalMinimum)func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {completionHandler(.NewData)}pero no he tenido los resultados deseados si me pueden ayudar a encotrar una solucion se los agradeceria

 

footer
jbelon © | sitemap.xml