Cómo usar el GPS en nuestras aplicaciones iOS 5 (iPhone, iPad, …)

0
7978

Creación: 18-04-2012

Índice de contenidos

1.Introducción
2. Entorno
3.Configurando el servicio
4.Preguntando nuestra posición
5.Cómo recupero la posición y sin gastar toda la batería
6.Cómo medir la distancia entre dos puntos
7.Cómo pintar un mapa con mi posición
8.Conclusiones
9. Sobre el autor

1. Introducción

Ahora prácticamente todos los dispositivos móviles llevan un GPS (Sistema de Posicionamiento Global), y esto ha permitido la aparición de innumerables aplicaciones que se sirven de esta posibilidad de localización para prestar mejor servicio a sus usuarios. Por ejemplo aplicaciones que nos dicen donde está el cajero o el cine más cercano, aplicaciones incluso que nos dicen donde están nuestros amigos, y por supuesto los conocidísimos
navegadores que nos guían en coche, moto o andando por ciudades y carreteras que desconocemos.

Por supuesto los dispositivos de Apple no son excepción y también vienen provistos de sistemas de localización, bien sea GPS, que sería el más preciso, u otros sistemas basados en la celda de telefonía en la que se encuentra nuestro móvil, o nuestra dirección IP de la red a la que estamos conectados, … al final, de una forma u otra y con más o menos precisión, somos capaces de geolocalizar nuestra posición.

Y a partir de aquí el límite es nuestra aplicación, por ejemplo el Zombie Outbreak Simulator que simula una infección de Zombies a nivel mundial 🙂

En este tutorial vamos a ver como podemos usar este tipo de servicios en iOS 5. Partiendo de la aplicación que ya hicimos en el tutorial de i18n (internacionalización).

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel i7, 8GB 1333 Mhz DDR3, 256GB Solid State Drive).
  • AMD Radeon HD 6770M 1024 MB
  • Sistema Operativo: Mac OS X Lion 10.7.3
  • Xcode 4.3.2
  • iOS 5.1


3. Configurando el servicio

Para poder usar el servicio tenemos que añadir el framework CoreLocation. Para ello pinchamos sobre el proyecto, sobre la pestaña de Build Phases, desplegamos Link Binary
With Libraries
, y pulsamos el + para añadir el nuevo framework.

Ahora buscamos la librería, con escribir el principio enseguida la tendremos localizada.

La seleccionamos y le damos al botón de añadir.

Por último sería interesante mover en el navegador de proyecto del Xcode el CoreLocation.framework dentro del grupo Framewoks. Esto no es obligatorio, pero si recomendable por tener las cosas un poco ordenaditas 🙂

Ahora que ya tenemos la librería, ahora podemos indicar en nuestro fichero Info.plist la clave UIRequiredDeviceCapabilities, para indicar que servicios de hardware necesitamos para ejecutar nuestra aplicación.
En relación con el posicionamiento tenemos las claves: location-services o gps, en
función de la precisión que necesitamos (todas las claves que podemos especificar aquí).

Ojo!!! Es importante que especifiquemos esta propiedad única y exclusivamente si nuestra
aplicación no puede funcionar de otra manera. Ya que si especificamos la propiedad estaremos filtrando en el App Store de forma que todos los dispositivos que no tengan la característica requerida no podrán instalarse la aplicación; disminuyendo así nuestros usuarios potenciales.

Por ejemplo, imaginemos que estamos mostrando una lista de cines ordenados por cercanía a mi posición actual. Si el dispositivo no tiene sistema de localización podríamos
mostrar la lista simplemente ordenada por orden alfabético, por lo que la aplicación seguiría funcionando correctamente. Con este pequeño “workaround” conseguimos que todo el mundo pueda instalar la aplicación y utilizarla, ampliando así nuestro posible mercado; y los que sí tengan el sistema de posicionamiento obtendrán un plus a la hora de usar nuestra aplicación.

Para usar los servicios de localización tan sólo tenemos que incluir:

#import <CoreLocation/CoreLocation.h>

Y para saber si están disponibles los servicios de localización en el dispositivo basta con preguntarlo con:

const BOOL enabled = [CLLocationManager locationServicesEnabled];

Este sería un pequeño test para comprobarlo (para saber un poquito más sobre como hacer test en Objective-C tenéis el material de mi charla en el XPWeek 2011):

#import "GpsTest.h"
#import <CoreLocation/CoreLocation.h>

@implementation GpsTest

- (void)testLocationServicesEnabled {
    const BOOL locationServicesEnabled = [CLLocationManager locationServicesEnabled];
    STAssertTrue(locationServicesEnabled, nil);
}
@end

 

Pero cuidado, que para que te funcione el código anterior no se te olvide activar los servicios de localización para esta aplicación. Para ello te puedes ir, en el iPhone o simulador, a Settings –> Location Services, y activarlo:

Activa los servicios de localización

 


4. Preguntando nuestra posición

Ya parece que lo tenemos todos listo, incluso hemos escrito un pequeño test (ver el código fuente del proyecto) donde comprobamos que nuestro dispositivo tiene disponibles los servicios de localización.

Lo primero que se suele hacer es preguntar por nuestra posición actual. Así que vamos a hacernos una pequeña vista donde juguetear con estas cosas.

Creamos una nueva clase que extienda UIViewController.
Para ello nos ponemos sobre el grupo iOSTutorial y hacemos File –> New –> File… (Cmd + N)

Crear una nueva clase

Nos aseguramos que está seleccionado Cocoa Touch y elegimos crear una nueva Objective-C class.

Ahora ponemos el nombre del controlador, podemos poner el que queramos (en mi caso GpsViewController), pero es muy importante digamos que será una subclase de UIViewController.

Crear un nuevo UIViewController

Por último le decimos que lo queremos guardar dentro del proyecto. Así que deberíamos ver algo del estilo de la siguiente imagen.

Código de un UIViewController recién creado

Ahora vamos al MainStoryboard.storyboard y vamos a crear la vista correspondiente a este controlador que acabamos de crear.

Vista para el UIViewController

En la imagen podemos ver como hemos seleccionado el primero de los iconos de la paleta de abajo a la izquierda. Este representa un View Controller normalito. Lo hemos arrastrado hacia el área de trabajo del storyboard y nos ha aparecido la nueva vista. Y por último arriba a la derecha hemos cambiado la clase de este View Controller para apuntar a la clase GpsViewController que hemos creado justo en el paso anterior.

Ahora vamos a añadir a esta visa una etiqueta donde mostrar las coordenadas de la posición. Y crearemos un Outlet para poder acceder a esta etiqueta desde el código (ya vimos como hacer un Outlet en el tutorial de i18n).

Outlet de la etiqueta para las coordenadasNótese en la imagen que también hemos cambiado la vista inicial de la aplicación. Esta se indica con la flecha que podemos ver abajo a la izquierda y que está apuntando a la nueva vista que hemos creado en este tutorial. Esto hace que no podamos acceder a lo que ya hicimos de i18n, pero por ahora nos vale. Ya veremos en otro tutorial como hacer la navegación entre vistas.

Hacemos lo mismo con otras dos etiquetas más, para mostrar las coordenadas iniciales y las coordenadas anteriores (nos llegará un evento de cambio de coordenadas donde nos indicarán las nuevas coordenadas y las antiguas).

Etiquetas con las coordenadas nuevas, antiguas e iniciales

Parece que ya tenemos preparada la parte de interfaz de usuario, es el momento de añadir el código a nuestra clase. Primero vemos el fichero GpsViewController.h

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>

@interface GpsViewController : UIViewController <CLLocationManagerDelegate> {
@private
    CLLocationManager *locationManager;
}
@property (strong, nonatomic) IBOutlet UILabel *gpsCoordinatesLbl;
@property (strong, nonatomic) IBOutlet UILabel *gpsOldCoordinatesLbl;
@property (strong, nonatomic) IBOutlet UILabel *gpsInitialCoordinatesLbl;

@end

Vemos como implementamos el protocolo CLLocationManagerDelegate.
Este protocolo define los métodos que serán llamados cada vez que haya un evento relacionado con la localización del dispositivo.

También hemos añadido un atributo a la clase para guardar una referencia al CLLocationManager.

Ahora el fichero GpsViewController.m

#import "GpsViewController.h"

@interface GpsViewController ()

@end

@implementation GpsViewController
@synthesize gpsCoordinatesLbl = _gpsCoordinatesLbl;
@synthesize gpsOldCoordinatesLbl = _gpsOldCoordinatesLbl;
@synthesize gpsInitialCoordinatesLbl = _gpsInitialCoordinatesLbl;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    locationManager = [[CLLocationManager alloc] init];
    locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    locationManager.delegate = self;

    [locationManager startUpdatingLocation];

    _gpsInitialCoordinatesLbl.text = [locationManager.location description];
    NSLog(@"Posición inicial: %@", locationManager.location);
}

- (void)viewDidUnload {
    [locationManager stopUpdatingLocation];

    locationManager = nil;
    [self setGpsCoordinatesLbl:nil];
    [self setGpsOldCoordinatesLbl:nil];
    [self setGpsInitialCoordinatesLbl:nil];

    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

#pragma mark - CLLocationManagerDelegate

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
    _gpsCoordinatesLbl.text = [newLocation description];
    _gpsOldCoordinatesLbl.text = [oldLocation description];
    NSLog(@"Posición anterior: %@   Posición actual: %@", oldLocation, newLocation);
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
    _gpsCoordinatesLbl.text = [manager.location description];
    NSLog(@"Error en el servicio de localización: %@", error);
}

@end

 

En el método viewDidLoad nos encargamos de crear y arrancar el sistema de localización. Para ello creamos la instancia y luego fijamos el valor de la propiedad desiredAccuracy.
Esta propiedad define la precisión que necesitamos en las medidas.
Los valores van desde pocos metros hasta varios kilómetros:

  • kCLLocationAccuracyBestForNavigation – Muy preciso porque usa varios sensores, pero pensado para usarlo sólo en aplicaciones de navegación, con el dispositivo enchufado a una fuente de alimentación.
  • KCLLocationAccuracyBest – Valor por defecto
  • kCLLocationAccuracyNearestTenMeters
  • kCLLocationAccuracyHundredMeters
  • kCLLocationAccuracyKilometer
  • kCLLocationAccuracyThreeKilometers

Tenemos que intentar usar siempre el valor menos preciso, ya que mientras más precisión más uso de batería porque se usan más funciones del hardware.

Hay otra propiedad que no estamos usando, distanceFilter.
Con esta propiedad podemos indicar cada cuantos metros queremos recibir un evento de cambio de localización. Es decir, mientras estemos dentro de ese rango indicado no recibiremos nuevos eventos de cambios de localización. Como en esta ocasión no indicamos valor para esta propiedad, estaremos recibiendo eventos de cambio de localización siempre que estos se produzcan, por pequeños que sean.

Es muy importante llamar al método startUpdatingLocation, porque de no hacerlo el dispositivo no encenderá los sistemas de localización y no recibiremos ningún evento al respecto.

Lo siguiente interesante que podemos ver en el código es lo que tenemos detrás del #pragma mark - CLLocationManagerDelegate.
Estamos implementando dos métodos del protocolo CLLocationManagerDelegate.

  • locationManager: didUpdateLocation: fromLocation: que es el método que será llamado cada vez que haya un nuevo evento de cambio de localización.
  • locationManager: didFailWithError: que será llamado si ocurre cualquier error con el sistema de localización.


5. Cómo recupero la posición y sin gastar toda la batería

Efectivamente, con el código que hemos hecho antes, la aplicación, mientras esté activa, no deja de recibir notificaciones de cambio de posición; y encima con una precisión muy alta! Esto va a causar que nuestra aplicación consuma mucha batería porque el
hardware del dispositivo tiene que estar continuamente encendido y atento a los cambios de posición.

A no ser que estemos haciendo alguna aplicación de tipo navegación este no suele ser el comportamiento necesario.
Normalmente basta con recuperar inicialmente la posición para que nuestra aplicación pueda funcionar (recordemos el ejemplo de localizar los cines más cercanos, con saber mi posición actual es más que suficiente).

Vamos a ver como cambiar el código para conseguir la posición inicial y parar de tomar muestras.

#import "GpsViewController.h"

@interface GpsViewController ()

@end

@implementation GpsViewController
@synthesize gpsCoordinatesLbl = _gpsCoordinatesLbl;
@synthesize gpsOldCoordinatesLbl = _gpsOldCoordinatesLbl;
@synthesize gpsInitialCoordinatesLbl = _gpsInitialCoordinatesLbl;
@synthesize totalCounterLbl = _totalCounterLbl;
@synthesize bestCounterLbl = _bestCounterLbl;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    bestLocation = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(0, 0)
                                                 altitude:0
                                       horizontalAccuracy:kCLLocationAccuracyThreeKilometers
                                         verticalAccuracy:0
                                                timestamp:0];
    totalCounter = 0;
    bestCounter = 0;

    locationManager = [[CLLocationManager alloc] init];
    locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    locationManager.delegate = self;

    [locationManager startUpdatingLocation];

    _gpsInitialCoordinatesLbl.text = [locationManager.location description];
    NSLog(@"Posición inicial: %@", locationManager.location);
}

- (void)viewDidUnload {
    [locationManager stopUpdatingLocation];

    locationManager = nil;
    bestLocation = nil;
    [self setGpsCoordinatesLbl:nil];
    [self setGpsOldCoordinatesLbl:nil];
    [self setGpsInitialCoordinatesLbl:nil];
    [self setTotalCounterLbl:nil];
    [self setBestCounterLbl:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - CLLocationManagerDelegate

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
    totalCounter++;
    if (newLocation.horizontalAccuracy < 0) return;
    if (-[newLocation.timestamp timeIntervalSinceNow] > 5.0) return;

    if (newLocation.horizontalAccuracy <= bestLocation.horizontalAccuracy) {
        _gpsOldCoordinatesLbl.text = [bestLocation description];
        _gpsCoordinatesLbl.text = [newLocation description];
        NSLog(@"Mejor posición anterior: %@   Mejor posición actual: %@", bestLocation, newLocation);

        bestLocation = newLocation;
        bestCounter++;

        if (newLocation.horizontalAccuracy <= kCLLocationAccuracyNearestTenMeters) {
            [locationManager stopUpdatingLocation];
            _gpsCoordinatesLbl.backgroundColor = [UIColor greenColor];
        }
    }
    _totalCounterLbl.text = [NSString stringWithFormat:@"%d", totalCounter];
    _bestCounterLbl.text = [NSString stringWithFormat:@"%d", bestCounter];
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
    _gpsCoordinatesLbl.text = [manager.location description];
    NSLog(@"Error en el servicio de localización: %@", error);
}

@end

Lo más interesante está en el método locationManager: didUpdateToLocation: fromLocation: donde básicamente hacemos:

  • Comprobar si es una localización válida viendo que la precisión horizontal no es negativa. Y comprobar que es una medida tomada recientemente (esto es porque al principio, hasta que empieza a funcionar el sistema de localización, puede ser que nos de la última posición cacheada).
  • Comprobamos si la última medida es más precisa que la que ya tenemos.
  • Y finalmente comprobamos si la última medida tiene la precisión que consideramos aceptable para nuestro caso. Si es así paramos el sistema de localización para que deje de consumir batería.

El resto del código es más menos el mismo, salvo que hemos añadido un par de etiquetas más a la interfaz de usuario para ver cuantas medidas se están tomando, y de estas cuantas son las que vamos considerando mejores (dos medidas con la misma precisión, la
más nueva de las dos la consideramos mejor por ser más actual).


6. Cómo medir la distancia entre dos puntos

Dentro de la clase CLLocation tenemos el método distanceFromLocation: que precisamente mide, en metros, la distancia entre dos localizaciones.

De esta forma si tenemos una lista de localizaciones sería bastante sencillo ordenarlas de menor a mayor distancia con respecto a nuestra posición actual. Bastaría con llamar a esta función por cada una de las localizaciones para obtener la distancia y luego ordenar la lista en función de estos resultados.

Ojo! Hay que tener en cuenta que distanceFromLocation: mide la distancia en línea recta, sin tener en cuenta algunas, calles o similares. Por lo que si queremos una medida más precisa no nos valdría y tendríamos que meternos a calcular el camino entre los dos puntos, para luego sumar la distancia de cada tramo del camino. Esto queda fuera de las intenciones de este tutorial.


7. Cómo pintar un mapa con mi posición

Es muy sencillo ya que existe una vista precisamente para hacer esto, así que basta con ir a nuestro MainStoryboard y añadir una Map View (MKMapView).

Añadir un MKMapView

Ahora vamos a indicar el delegate del MKMapView. Esto nos va a servir para el map view nos llame cuando tenga que pintar determinadas cosas, como las anotaciones (pins que situamos en el mapa), y así podamos alterar un poco la forma en la que se pintan.

Indicar el delegate para el MKMapView

Ahora hacemos un Outlet de esta vista para poder usarla en nuestro código. Veremos que tras hacer el Outlet tenemos un error de compilación porque no reconoce el tipo MKMapView, eso es porque necesitamos añadir el framework MapKit.framework (ya vimos en este mismo tutorial como se añade un nuevo framework, así que basta con seguir los mismos pasos).

Luego tendremos que añadir el correspondiente include en el fichero GpsViewController.h

#import <MapKit/MapKit.h>

 

El fichero GpsViewController.m lo hemos cambiado para que cuando encuentre nuestra posición pinte un pin verde en el mapa.

#pragma mark - CLLocationManagerDelegate

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
    totalCounter++;
    if (newLocation.horizontalAccuracy < 0) return;
    if (-[newLocation.timestamp timeIntervalSinceNow] > 5.0) return;

    if (newLocation.horizontalAccuracy <= bestLocation.horizontalAccuracy) {
        _gpsOldCoordinatesLbl.text = [bestLocation description];
        _gpsCoordinatesLbl.text = [newLocation description];
        NSLog(@"Mejor posición anterior: %@   Mejor posición actual: %@", bestLocation, newLocation);

        bestLocation = newLocation;
        bestCounter++;

        if (newLocation.horizontalAccuracy <= kCLLocationAccuracyNearestTenMeters) {
            [locationManager stopUpdatingLocation];
            _gpsCoordinatesLbl.backgroundColor = [UIColor greenColor];

            [self createBestLocationAnnotation];
        }
    }
    _totalCounterLbl.text = [NSString stringWithFormat:@"%d", totalCounter];
    _bestCounterLbl.text = [NSString stringWithFormat:@"%d", bestCounter];
}

- (void)createBestLocationAnnotation {
    MKPointAnnotation *currentLocationPin = [[MKPointAnnotation alloc] init];
    currentLocationPin.coordinate = bestLocation.coordinate;
    currentLocationPin.title = NSLocalizedString(@"Actual position", @"Actual position");
    currentLocationPin.subtitle = NSLocalizedString(@"Yeah!", @"Yeah!";);
    [_mapView addAnnotation:currentLocationPin];
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
    _gpsCoordinatesLbl.text = [manager.location description];
    NSLog(@"Error en el servicio de localización: %@", error);
}

#pragma mark - MKMapViewDelegate

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
    MKAnnotationView *pin = [mapView dequeueReusableAnnotationViewWithIdentifier:@"currentLocation"];
    if (pin == nil) {
        MKPinAnnotationView *pinAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"currentLocation"];
        pinAnnotationView.annotation = annotation;
        pinAnnotationView.pinColor = MKPinAnnotationColorGreen;
        pinAnnotationView.animatesDrop = TRUE;
        pinAnnotationView.canShowCallout = TRUE;
        pin = pinAnnotationView;
    }
    return pin;
}

Lo más destacable es

  • la función createBestLocationAnnotation, donde creamos la anotación una vez encontrada la posición,
  • y la implementación de mapView: viewForAnnotation: donde devolvemos al map view una MKAnnotationView, que en nuestro caso es un pin verde animado, con un texto.

Si ejecutamos la aplicación veremos algo del estilo:

Pin verde en Autentia!

 

8. Conclusiones

Sólo hemos visto una introducción a todas las posibilidades de localización que tenemos en el iOS. Podemos hacer muchas más cosas, como detectar los cambios de localización sólo cuando cambiamos de celda del teléfono, y así conservar mucho mejor la batería; o indicar un rango en metros para que el sistema nos avise sólo cuando salimos de ese radio; o indicar al sistema de localización que nos avise cuando entramos o salimos de una región determinada; o como estar escuchando eventos de cambio de localización mientras la aplicación está suspendida, …

Pero con este tutorial ya tenemos unas buenas bases para seguir investigando y dotar a nuestras aplicaciones de estas fabulosas características.

Y ya sabéis, acordaos de apagar los sistemas de localización siempre que no los necesites para así ahorrar batería.

Os dejo todo el código del tutorial en este repositorio de GitHub, en la etiqueta gps.

9. Sobre el autor

Alejandro Pérez García, Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster

Socio fundador de Autentia (Desarrollo de software, Consultoría, Formación)

mailto:alejandropg@autentia.com

Autentia Real Business Solutions S.L. – «Soporte a Desarrollo»

http://www.autentia.com

 

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad