En este artículo cuento mi experiencia de creación de la aplicación de iOs TimeBox ,desde el estudio de mercado, diseño, y elaboración de la interfaz gráfica hasta la programación, creación de la base de datos y la aplicación web para redes sociales.
Lo primero que debemos hacer es tener claro qué tipo de aplicación queremos crear, para eso hay que tener una buena idea, y si la tenéis pero la aplicación ya está hecha, entonces comprar/bajaros todas las aplicaciones que han implementado vuestra idea y estudiarlas para encontrar en qué fallan, cómo las podéis mejorar y combinar, es decir, encontrad una motivación para programar, porque esto es duro amigos jeje (más…)
Esta aplicación surgió como un proyecto personal a partir de la necesidad de crear cartas astrales.
El reto comenzó con la forma en que se han realizar los cálculos de una carta astral. Por suerte, hay un software escrito en C, de los alemanes Dieter Koch and Alois Treindl y Astrodienst Zurich llamado SWISSEPH, conecté por consola ssh con mi servidor, utilicé wget con la última versión ( 1.76 ) y tras el make probé Swetest que a partir de una serie de argumentos de entrada muestra por pantalla un “ephemeris”, el resultado de leer una base de datos de posiciones de planetas, asteroides, estrellas, etc. y devolver las posiciones para cada una de las casas de dicha fecha y hora en el lugar especificado por longitud y latitud.Este programa además lanza una serie de datos para generar gráficas como “spreadsheets”.
Usando estos datos y un pequeño script:
//Analizar sintácticamente la fecha,hora,longitud y latitud exec ("swetest -edir../src/ -b$utdatenow -ut$utnow -p0123456789 -eswe -house$longitud,$latitud, -fPlj -g, -head", $salida); foreach ($salida as $key => $linea) { $row = explode(',',$linea); $pl_name[$key] = $row[0]; $longitude[$key] = $row[1]; $house_pos[$key] = $row[3]; };
Donde cada línea de salida de swetest se divide en arrays $row, con los elementos:0 = planeta,1 = longitud,2 = posición de casa, planetas en 0 – 9, cúspides en 10 – 21. Los nombres de los componentes,empiezan por 0 = Sol, que es la estrella del sistema solar,junto con la Luna=1 y Mercurio = 2 hasta Plutón = 9, los símbolos serían del 1 = Aries hasta 12 = Piscis. Para asociar la longitud y latitud al signo se hace una regla de 30 que limita la longitud y la combina con la casa asociada que devuelve el programa, de esta forma, podemos generar una imagen con instrucciones de dibujado geométrico (círculos, líneas, símbolos como texto)…Podéis ver un ejemplo aquí.
Para generar textos más completos he utilizado webs que los devuelven gratuitamente a partir de los datos, como grupovenus.
Finalmente, con un script en Python dentro de Google App Engine se crean todas las consultas en segundo plano, se construye el XML con HTML+CSS embebido, imágenes, etc. y se guardan, previo análisis sintáctico mediante la clase NSXMLParser en la base de datos SQlite asociada al Modelo de Datos, tal como vimos en los cursos.
Las animaciones de la app: las transiciones están hechas en OpenGL (HMGLTransition) y con las clases CAAnimation. El menú giratorio es una extensión de la clase UIGestureRecognizer ( KTOneFingerRotationGestureRecognizer ) modificada adaptando cada sección a un ángulo con una animación UIView…
Para la base de datos, primero se genera el modelo de datos, luego se compila y ejecuta la aplicación para que el mismo SDK cree el fichero SQlite así, tenemos la base sobre la que insertar los datos, una vez hemos rellenado todas las tablas, copiamos el fichero sqlite al proyecto y con un código como este:
NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"Carta_Astral.sqlite"]; // Set up the store. // For the sake of illustration, provide a pre-populated default store. NSFileManager *fileManager = [NSFileManager defaultManager]; // If the expected store doesn’t exist, copy the default store. if (![fileManager fileExistsAtPath:storePath]) { NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"Carta_Astral" ofType:@"sqlite"]; if (defaultStorePath) { [fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL]; } }
la primera vez que se ejecuta la aplicación, se copia al directorio de Documentos y ahí es donde se graban los datos del usuario, hago especial énfasis en que los tipos de datos complejos como Arrays de imágenes, textos y nombres se guardan en un tipo de dato del Modelo de Datos de Cocoa llamado Transformable.
En la sección de manuales hay tres tipos de manuales, los vídeos de youtube que se cargan sobre un objeto WebView, los textos con imagen que se cargan sobre una vista a mano y los pdf’s que se descargan a una caché con ASIHttp DownloadCache…Para paginar todos los documentos, libros, vídeos he usado una clase que crea un número infinito (gracias Andreas Katzian) de ScrollViews y los guarda en una caché de vistas para no tener que ir generándolos cada vez que se cambia de página, de forma que sólo se consulta una vez a la base de datos, se guarda una caché de tuplas y luego una caché de vistas con sus correspondientes botones, etc., así es mucho más eficiente y sencillo.
Para compartir textos e imágenes se utilizó ShareKit.
Veréis que las barras de navegación y las barras botones (tab) tienen una textura de fondo, esto se hace sobrecargando la clase correspondiente, en concreto el método de dibujado, para hacer que pinte una imagen por debajo y luego el resto del contenido.
La aplicación está siendo desarrollada
Recientemente he tenido que revisar un portal realizado con el gestor de contenidos ( CMS ) y framework PHP: CodeIgniter, como base y ExtJS como framework JavaScript para proporcionar los servicios de datos por medio del prototipo JSON. Dividiré el análisis en tres partes: Framework JavaScript, Base de datos y Aplicaciones PHP.
Había encontrado algunos problemas, por ejemplo, si entramos en la web con un dominio sin las tres uves dobles, al estar configurado para el dominio con ellas, los ficheros fuente no eran bien referenciados y había errores por todas partes. Estos bugs se corrigieron fácilmente, pero hay que entender o bien por experiencia, por intuición o por inteligencia, de dónde vienen dichos problemas para poder solucionarlos.
El sistema a analizar tiene un directorio privado, sin embargo se ha de crear una lógica de roles de usuario y ficheros de acceso restringido o bien con PHP o bien con .htaccess para que ningún usuario no autorizado a una zona pueda acceder a la información privada que reside en regiones que no le pertenecen.
El primer error que encontré en el diseño de la aplicación no
estaba en la base de datos, ya que el diseño de entidad relación está correcto, dentro de lo que cabe , y he visto verdaderas barbaridades por ahí…como decía, el primer error es la propia interfaz de usuario, mirad este vídeo:
Sobre si la decisión de usar Ext JS u otro framework JavaScript como JQuery la dejo a elección del programador porque realmente depende de cómo de cómodo te sientas escrbiendo las piezas del puzzle del portal, en este caso está bastante bien porque se utiliza Firebug para depurar PHP con una consola de estado de Ext JS. Aunque es un poco raro mezclar opensource con freesource, es una buena manera de construir un portal complejo, eso sí, siempre que tengamos claro qué es lo que queremos construir…
var Center = Ext.extend(Ext.util.Observable, { constructor : function(ui) { this.ui = ui; this.panel = new Ext.TabPanel({ title : 'Center', region : 'center', activeTab : 0, autoDestroy : false, items: [ { title:'Bienvenido', closable : false, bodyStyle :'font-family:sans-serif;font-size:12px;', html :'[contenido]' }] }); } });
En [contenido] debemos colocar el contenido del componente, por ejemplo, un iframe o cualquier otro elemento html que queramos…
Por ejemplo, para añadir una pestaña con lo que se debe mostrar de un cliente una opción es la siguiente:
MisClientes = Ext.extend(Ext.Panel,{ // Prototype Defaults propA: 1, title: 'Mis Clientes', closable : true, iconCls: 'icon-mis-clientes-tab', initComponent: function(){ // Called during component initialization this.fields =[ {name:'cliente', type:'string'}, {name:'nombre', type:'string'}, {name:'email', type:'string'} ]; this.storeGrid = new Ext.data.Store({ reader:new Ext.data.JsonReader({ idProperty:'cliente', totalProperty:'totalCount', root:'rows', fields: this.fields }), proxy:new Ext.data.HttpProxy( {url: BASE_URL + 'asignarEntrenador/load_clientes_asig'}), baseParams:{entrenador:USER_LOGGED}, remoteSort:true }); //...etc.
De esta forma hemos encapsulado toda la información de un cliente junto con los widgets que lo representan ,en un único objeto, sin embargo todo el trabajo no acaba ahí, hay que escribir también los prototipos de respuesta para cada una de las acciones, que devolverán otros widgets, elementos de sincronización, alimentadores de datos o datastores, etc. A partir de aquí hay que sumergirse en el mundo del framework Ext JS, y…
Para terminar con el análisis de la sección del framework JavaScript, las plantillas que este utiliza se han guardado separadas del motor PHP (framework CodeIgniter), esto es buena idea porque se puede actualizar fácilmente y además podríamos crear un subdominio para aligerar la carga de la web de forma que realizara las peticiones de todo el contenido JS en paralelo desde el navegador.
Hablando de la base de datos, como había comentado antes, no está nada mal, aquí vemos el diagrama de entidad/relación, los usuarios son la tabla principal del sistema y alrededor de ella se construye todo el conjunto de entidades que abordan las necesidades del cliente, el administrador ,etc.
Correcciones a aplicar sobre la base de datos: se debe modificar el campo usuario de la tabla de usuarios para que sea único, algunos tipos de datos son demasiado grandes para la información que almacenan, hay que tener en cuenta que cuando el número de usuarios crece y con esto también los datos asociados, la sinergía de nuestro servidor de base de datos depende tanto del número de transacciones como del volumen de estas y si estamos usando un framework que realiza una petición de todos los datos de un usuario para mostrarlos con datos asociados por cada prototipo de entidad, el volumen de estos datos genera un considerable tamaño de ancho de banda que puede ralentizar la aplicación. Para esto se construyen consultas que se guardan como vistas y otras consultas avanzadas como las que se pueden realizar con PL/SQL.
En principio la base de datos no necesitaba más cambios, sólo quedaba analizar si estaba preparada para la expansión, es decir, si era escalable, para probarlo, me ví en la tesitura de añadir una tabla que almacenara información relacionada con archivos compartidos entre usuarios de distintos privilegios, y en este caso no tuve ningún problema con la base de datos sino con el siguiente punto ,ya que la lógica de programación no estaba terminada, no había diferenciación fuerte entre roles y aquí fué donde me llevé la gran sorpresa de toda la jerarquía.
El lector seguramente ,como programador sabrá que los tipados fuertes (objetos) siempre son adecuados para embeber información de un usuario, de modo que con una simple lectura de una propiedad de la instancia de la clase usuario->tipo sabremos qué tipo de usuario es…claro, la cosa se complica al tener que replicar la información en un framework como Ext JS, sabemos que podemos encriptar la información pero finalmente,para acometer una acción delicada debemos realizar la comprobación del nivel de privilegios de un usuario por duplicado: en el framework JS y en el framework PHP.
Llegados al punto en que la jerarquía MVC se abría ante mí, todo parecía el paraíso, hay modelos,
class Usuarios extends Model { public function __construct() { parent::Model(); // Call the Model constructor $this->table_usuarios = 'usuarios'; $this->table_usuarios_tipo = 'usuarios_tipo'; $this->table_session = 'dasm_sessions'; //$this->load->library('firephp'); } //TODO: Comprobar que es un admin function crear(){ //etc.
qué es eso “TODO: Comprobar que es un admin”?…sigamos analizando, hay controladores…bien!
class User extends Controller { public function __construct() { parent::Controller(); if (function_exists('force_ssl')) force_ssl(); $this->load->library('session'); //iniciar libreria de sesiones $this->load->library('firephp'); //FIREBUG $this->firephp->log("force shhl"); } public function index(){ $this->login(); } public function login() { if ($this->my_usession->logged_in) { $this->firephp->log("login"); //....
es un poco extraño, -me digo para mí- , ahora es todo coser y cantar, pero no…me parece que el programador se acaba de ventilar toda la jerarquía de roles…además, no hay vistas asociadas,ni controladores en los hooks de C.I., pánico…todas las acciones se realizan por real decreto de…llamadas incoherentes dentro de ficheros javascript?…ok, he muerto, ahora he de renacer a la realidad…¿cómo arreglaríais este desastre organizativo?,
Opción básica A: operar sobre la chapuza a sabiendas de que cada vez se enredará más y más el código
Opción B sólo para los valientes: intentar arreglar toda la jerarquía de clases, añadir los hooks, la lógica de roles y rezar para que no haya ninguna incoherencia…en peores batallas hemos estado
Opción C: la gran elegida por el público y aclamada por todos los directores de proyecto solidarios con la causa del programador medio : rehacer el sistema, esta vez BIEN HECHO.
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:
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; } }); }); }
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.
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 posicionamientoiOS nos permite conseguir la localización actual con diferentes métodos de posicionamiento:
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,
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.
- (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: @"http://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]>=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:
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