Nos adentramos en aprender algunas de las novedades más destacadas que nos trae el conocido framework de mapas MapKit en iOS 7.
Novedades en MapKit
Con la llegada del iOS 7, se han añadido nuevas funcionalidades a los mapas de Apple, como vista 3D, un objeto cámara para que podamos movernos programáticamente por el “mundo”, capturas de trozos de mapa y una de las más importantes, MKDirections, que nos permitirá obtener y pintar sobre el mapa rutas entre distintos puntos. Con todo esto los desarrolladores disponemos de más herramientas que nos ayudarán a conseguir mejores resultados a la hora de trabajar con mapas en nuestras apps.
El proyecto de ejemplo usado en este post, podéis descargarlo en el siguiente enlace:
https://github.com/Juanpe/MapKitSample_iOS7
Centrar mapa
En versiones anteriores, si queríamos centrar y ajustar la altura del mapa para que se vieran unos puntos concretos, teníamos dos opciones; o bien creábamos una región y un radio e íbamos probando o nos currábamos un método que a partir de esa nube de puntos calculase dicha región automáticamente.
En iOS 7 se ha añadido un método que se encarga de esto, es decir, pasándole una nube de puntos, automáticamente calcula la región y se centra y escala para que todos los puntos estén visibles.
Un ejemplo de su implementación sería:
1 2 3 4 5 6 7 8 9 10 11 |
MKPointAnnotation * point1 = [[MKPointAnnotation alloc] init]; point1.coordinate = CLLocationCoordinate2DMake(51.509194, -0.087022); point1.title = @"London"; MKPointAnnotation * point2 = [[MKPointAnnotation alloc] init]; point2.coordinate = CLLocationCoordinate2DMake(51.506568, -0.143225); point2.title = @"Piccadily"; [self.mapView showAnnotations:@[point1, point2] animated:YES]; |
Mapas 3D y MKMapCamera
Desde que los mapas de Apple vieron a luz, han contado con la opción de poder verse en 3D. Sin embargo, los desarrolladores aún no podíamos hacer uso de esa característica en nuestras apps.
MKMapCamera es una de las nuevas clases que incorpora MapKit y que nos permitirá movernos por el mapa por código.
Las propiedades más relevantes y útiles de esta clase son:
- centerCoordinate: Un punto del plano en el que queremos centrarnos.
- altitude: Altura de la cámara respecto al plano.
- pitch: Es el ángulo de la cámara. Cuando está a 0 es como si mirásemos hacia abajo.
Jugando con estas propiedades conseguimos el efecto 3D, además de transportarnos de un sitio a otro, muy útil por ejemplo si estamos mostrando la ruta que debe seguir un usuario hasta llegar a su destino en tiempo real.
En el proyecto de ejemplo adjunto en este post, podemos ver un ejemplo de cómo alternar entre la vista 2D y 3D con un UISegmentedControl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
- (IBAction) selectorMapTypeValueChanged:(id)sender { NSInteger optionSelected = [((UISegmentedControl *)sender) selectedSegmentIndex]; // // Capturamos la cámara que se está usando para modificarla // y volverla a añadir. Realmente no sería necesario, ya que // podemos crear una desde cero. // MKMapCamera* currentCamera = self.mapView.camera; switch (optionSelected) { case JCMap2DIndex: currentCamera.pitch = 0.0f; break; case JCMap3DIndex: currentCamera.altitude = 200.0f; currentCamera.pitch = 70.0f; break; default: currentCamera.pitch = 0.0f; break; } [self.mapView setCamera:currentCamera animated:YES]; } |
El resultado de este método es:
Podríamos haber creado una cámara nueva en vez de obtener la que ya tenía asignada el mapa. El motivo de usar la actual es porque cada cámara debe tener una ubicación (coordenada) y como realmente solo queremos alternar entre 2D y 3D, la coordenada es la misma, así que nos aprovechamos de la que tenemos y solo modificamos los parámetros para el 3D.
La vista 3D de los mapas solo está disponible para el tipo Standard (MKMapTypeStandard)
MKOverlayView (Deprecated in iOS 7.0. Use an MKOverlayRenderer object instead.)
Así es, como dice el título, la clase MKOverlayView, que permitía dibujar líneas, polígonos, formas, etc encima de los mapas, ha sido deprecada en iOS 7. En su lugar debemos usar MKOverlayRenderer. Una clase más eficiente que en lugar de heredar de UIView, hereda de NSObject directamente.
Como consecuencia de este cambio, a todos los objetos que implementaban el protocolo MKOverlay (protocolo necesario para que una vista cualquiera se pudiese pintar encimar de los mapas) han sido renombrados, por ejemplo MKPolygonView ha sido deprecado y en su lugar hay que usar MKPolygonRenderer.
En el proyecto de ejemplo pintamos una región uniendo 4 puntos de interés. El código es:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
- (IBAction) drawRegionWithPointsTouchUpInside{ // // Borramos las vistas que estén pintadas // en el mapa. // [self _resetOverlayViews]; // // Centramos el mapa para que se vean // todos los puntos. // [self _centerMapWithCoords:@[ [self _locationWithCoordinate:self.LondonBridgeCoords], [self _locationWithCoordinate:self.LondonCityCoords], [self _locationWithCoordinate:self.PiccadilyCoords], [self _locationWithCoordinate:self.ElizabethTowerCoords], ]]; CLLocationCoordinate2D points[4]; points[0] = self.LondonBridgeCoords; points[1] = self.LondonCityCoords; points[2] = self.PiccadilyCoords; points[3] = self.ElizabethTowerCoords; // // Creamos un polígono con las coordenadas y // lo guardamos. // self.regionPolygon = [MKPolygon polygonWithCoordinates:points count:4]; // // Lo añadimos al mapa. // [_mapView addOverlay:self.regionPolygon]; } |
Obteniendo como resultado:
MKGeodesicPolyline
Con MKPolyline podíamos pintar una línea en el mapa de un punto hasta otro. En iOS 7, se ha añadido un nuevo tipo, cuyo funcionamiento es igual al MKPolyline con la única diferencia de que a la hora de pintarse en el mapa, en vez de pintarse como una recta, esta nueva línea adopta una forma curva, siguiendo la curvatura de la tierra.
Resulta bastante útil para representar trayectorias de vuelos.
Un ejemplo de uso:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[self _resetOverlayViews]; [self _centerMapWithCoords:@[ [self _locationWithCoordinate:self.LondonCityCoords], [self _locationWithCoordinate:self.NewYorkCoords], ]]; CLLocationCoordinate2D points[2]; points[0] = self.LondonCityCoords; points[1] = self.NewYorkCoords; self.geodesicPolyline = [MKGeodesicPolyline polylineWithCoordinates:points count:2]; [_mapView addOverlay:self.geodesicPolyline]; |
Z-Index en los Overlays
Otra de las novedades es que podemos decidir la importancia de lo que estamos pintando en el mapa, es decir, podemos ordenar las capas para que lo que añadamos se vea por encima de toda la info nativa de los mapas o no.
Para ello MKMapView cuenta con una variable enumerada MKOverlayLevel, cuyas valores son:
- MKOverlayAboveRoads: Pinta nuestra vista por encima de las carreteras pero por debajo de los puntos de interés, etiquetas, etc.
- MKOverlayAboveLabels: Pinta nuestra vista por encima de cualquier otro elemento del mapa.
Este nivel se define a la hora de añadirlo al mapa:
1 2 3 |
[_mapView addOverlay:self.geodesicPolyline level:MKOverlayLevelAboveLabels]; |
Personalizar overlays
Esto no es nuevo, es decir, en los anteriores SDKs se podía también personalizar las polylines. La única novedad es que el método ha sido renombrado:
Antes:
1 2 3 |
- (MKOverlayView *)viewForOverlay:(id < MKOverlay >)overlay |
Ahora:
1 2 3 |
- (MKOverlayRenderer *)rendererForOverlay:(id < MKOverlay >)overlay |
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
- (MKOverlayRenderer *) mapView:(MKMapView *)mapView rendererForOverlay:(id)overlay{ UIColor *blueColorTranslucent = [UIColor colorWithRed:0 green:0 blue:255.0f alpha:0.5f]; if(overlay == self.regionPolygon){ // // Si pintamos la region, línea azul // con grosor de 1 y relleno azul con transparencia. // MKPolygonRenderer *polygonRenderer = [[MKPolygonRenderer alloc] initWithOverlay:overlay]; polygonRenderer.strokeColor = [UIColor blueColor]; polygonRenderer.fillColor = blueColorTranslucent; polygonRenderer.lineWidth = 1.0f; return polygonRenderer; }else if(overlay == self.overlayPolyline || overlay == self.geodesicPolyline){ // // Si estamos pintando una línea punto a punto // la pintamos de rojo y grosor 3 // MKPolylineRenderer *polylineRender = [[MKPolylineRenderer alloc] initWithOverlay:overlay]; polylineRender.lineWidth = 3.0f; UIColor *lineColor = [UIColor redColor]; if(overlay == self.overlayPolyline){ lineColor = [UIColor blackColor]; } polylineRender.strokeColor = lineColor; return polylineRender; }else{ // // Pintamos la ruta en azul y grosor 3 // MKPolylineRenderer *polylineRender = [[MKPolylineRenderer alloc] initWithOverlay:overlay]; polylineRender.lineWidth = 3.0f; polylineRender.strokeColor = [UIColor blueColor]; return polylineRender; } return nil; } |
Rutas punto a punto
Junto con la vista 3D, considero que es la mejora más significativa que trae consigo iOS SDK 7. A partir de ahora podemos preguntarle a los mapas de Apple, cual es el camino que debemos seguir para llegar desde el punto A hasta el punto B.
Para ello tenemos que crear una request con un punto inicial y un punto final:
1 2 3 4 5 |
MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init]; request.source = source request.destination = destination; |
Donde “source” y “destination” son del tipo MKMapItem, objetos creados a partir de MKPlacemark:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// // Placemark // MKPlacemark *fromPlacemark = [[MKPlacemark alloc] initWithCoordinate:self.LondonBridgeCoords addressDictionary:nil]; MKPlacemark *toPlacemark = [[MKPlacemark alloc] initWithCoordinate:self.PiccadilyCoords addressDictionary:nil]; // // Map Item // MKMapItem *fromItem = [[MKMapItem alloc] initWithPlacemark:fromPlacemark]; MKMapItem *toItem = [[MKMapItem alloc] initWithPlacemark:toPlacemark]; |
Una vez que hemos creado la request, solo tenemos que lanzarla a través de MKDirections y esperar el resultado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
MKDirections *directions = [[MKDirections alloc] initWithRequest:request]; [directions calculateDirectionsWithCompletionHandler: ^(MKDirectionsResponse *response, NSError *error) { if (error) { NSLog(@"Error => %@", error.debugDescription); }else { MKRoute *route = response.routes[0]; [self _drawRoutePolyline:route.polyline]; } }]; |
Además, la cosa no se queda en que podamos obtener la ruta sin más, sino que podemos definir una serie de opciones a la hora de crear la request para afinar aún más nuestra ruta:
- transportType: Como su nombre indica, representa el tipo de transporte que vamos a usar para hacer la ruta. Los posibles valores son: MKDirectionsTransportTypeAutomobile: En coche, MKDirectionsTransportTypeWalking: Andando, y MKDirectionsTransportTypeAny: Sin definir (Por defecto)
- requestAlternateRoutes (BOOL): Nos permite pedirle a Apple rutas alternativas. Por defecto su valor es NO.
- departureDate (NSDate): Indica la hora en la que queremos iniciar la ruta.
- arrivalDate (NSDate): Indica la hora a la que queremos/debemos llegar a nuestro destino.
Pero espera que hay más, a parte de darnos una ruta o rutas, dependiendo de si hay alternativas o no, Apple nos devuelve un array con todos los pasos que tenemos que seguir. Vamos a revisar las propiedades del objeto MKRoute, que antes hemos pasado por alto, para ver su potencial:
MKRoute
- polyline (MKPolyline): Polyline para pintarla en el mapa.
- name (NSString): Nombre asociado a la ruta.
- advisoryNotices (NSArray): Un array con strings indicando una serie de alertas que el usuario debe tener a la hora de hacer la ruta. Destacar que los strings están localizados, es decir, son devueltos en el idioma en el que esté el dispositivo.
- distance (CLLocationDistance): Distancia total de la ruta en metros.
- expectedTravelTime (NSTimeInterval): Tiempo total que se tardará en hacer la ruta.
- transportType: Tipo de transporte usado en la ruta.
- steps (NSArray): Array con todos los pasos a seguir por el usuario. Los pasos están definidos con una nueva clase MKRouteStep.
MKRouteStep
- polyline (MKPolyline): Polyline del tramo.
- instructions (NSString): Instrucciones para este tramo. Localizadas.
- notice (NSString): Alerta, en el caso de que la haya, para avisar al usuario de algún peligro en el tramo. Localizadas.
- distance (CLLocationDistance): Distancia total de este tramo en metros.
- transportType: Tipo de transporte usado en este tramo.
Una vez que hemos explicado como obtener la ruta y que resultados obtenemos, podéis ver un ejemplo completo de uso en el proyecto usado de ejemplo.
Capturas de trozos de mapa
Por último, hablamos de la nueva “cámara de fotos” que Apple ha añadido a sus mapas. En iOS 7 disponemos de la clase MKMapSnapshotter, que se encarga de capturar un trozo de mapa a partir de una serie de opciones que nosotros tenemos que proporcionarle a la hora de crear el objeto en cuestión.
Un ejemplo básico de su uso podría ser:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init]; options.size = CGSizeMake(256, 360); options.scale = [[UIScreen mainScreen] scale]; options.camera = self.mapView.camera; options.mapType = MKMapTypeStandard; MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options]; [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *e) { if (e) { NSLog(@"Error => %@", e.debugDescription); } else { UIImage* mapCaptured = snapshot.image; } }]; |
Como podéis ver, su uso es muy similar al de UIImagePickerViewController, es decir, la cámara o la galería de fotos. Creamos el manager, lo mostramos y a través de un bloque nos devuelve o bien un error o un objeto MKMapSnapshot que contiene un UIImage con la captura obtenida.
Vamos a ver un poco por encima las opciones que podemos definir, ya que tampoco hay mucho más misterio dentro de esta clase.
- camera: Objeto cámara (cámara virtual asociada al mapa) que vamos a usar para hacer la captura.
- region: Definimos este parámetro si queremos capturar una región concreta del mapa.
- mapType: Tipo de mapa.
- showsPointsOfInterest: Indica si queremos mostrar los POIs en la captura.
- showsBuildings: Indica si queremos mostrar los edificios en la captura. Para esto es necesario que el pitch de la cámara sea mayor que 0 y que el tipo de mapa sea estándar.
- size: Dimensión a nivel de pixel de la captura.
- scale: Escala de la captura.
Pues hasta aquí hemos llegado. He intentado explicar lo mejor posible todas las nuevas mejoras que incorpora MapKit y espero que os sea de ayuda para vuestros proyectos. Espero también que si tenéis alguna sugerencia de mejora la pongáis en los comentarios, será bienvenida ☺
Hasta la próxima!
Juanpe
Hola Miguel leyendo el articulo tan interesante me ha surgido una duda, soy algo principiante entonces no te enfades jajaj
Mi pregunta es como impleneto estas novedades, es decir, cuando yo quiero crear una ruta en mi MKView donde implemento el codigo que pones en la aplicacion para hacer una ruta por ejemplo
En el .m?
Buenas Miguel,
Toda la programación tienes que hacerla en los .m ya que los .h son solo declaración de interfaces.
Puedes descargarte desde github el código del ejemplo y así poder ver donde hacer las programaciones.
Hola, muy buen artículo. Me entran unas dudas…
Esta ruta, es posible que si vas en coche te guía con voz paso a paso como un Tom Tom?
En caso negativo, se le pueden pasar los parámetros de la ruta a la aplicación de mapas de Apple u otro para que te lleve de un punto a otro indicándote la ruta con voz?
Gracias y un saludo.
Podrías usar la librería que está disponible desde iOS 7, AVSpeechSynthesizer. Te dejo el link
Con esta librería puedes sintetizar cualquier texto y así poder guíar al usuario con voz.
Saludos
Gracias. Lo probaré.
Lo logre y probado. Para que en los dispositivos salga las instrucciones en el idioma que quieres no tienes que cambiar el idioma del dispositivo sino de la aplicación. Tampoco fue fácil ver en cual de todos era, estuve a punto de darme por vencido. La respuesta. Tienes que seleccionar el proyecto (No el target). Hay dos pestañas Info y Build Settings, selecciona Info. Busca Localizations, agrega uno en el símbolo de mas y deja seleccionado los idiomas que te aparezcan. Después elimina el de ingles para que solo quede el de español. Prueba la aplicación y ta tan, ya esta.
Tengo el mismo problema de las instrucciones en ingles. En el emulador lo cambie en settings a español y me sale en español. Pero probando en dispositivos reales siempre sale en ingles aunque el idioma este en español. hay alguna manera de forzar que las instrucciones salgan en español??? Agradeciendo de antemano los días de trabajo que me has ahorrado con este código. Gracias
Enhorabuena por el artículo, muy bien explicado todo.
Tan sólo he tenido una contrariedad: los strings «instructions» de las rutas punto a punto no vienen localizados, siempre me salen en inglés, independientemente del idioma el dispositivo, ya sea en simulador o en un dispositivo real.
¿A alguien le ha funcionado la localización de las instrucciones?
Un saludo y gracias.
Hola Juanpe, excelente articulo, me sirvió la primera parte, pero soy nuevo en Xcode y quiero agregar 30 pins en un mapa pero copiando el código que entregas, sólo me merca 2 puntos y los demás los muestra en meridiano y paralelo 0. Que puedo estar haciendo mal?
MKPointAnnotation * point1 = [[MKPointAnnotation alloc] init];
point1.coordinate = CLLocationCoordinate2DMake(-27.3678213,-70.3317451);
point1.title = @"O`Higgins 750";
point1.subtitle = @"$800";
MKPointAnnotation * point2 = [[MKPointAnnotation alloc] init];
point2.coordinate = CLLocationCoordinate2DMake(-27.3689492,-70.3323781);
point2.title = @"Atacama 760";
point2.subtitle = @"$1.000";
MKPointAnnotation * point3 = [[MKPointAnnotation alloc] init];
point1.coordinate = CLLocationCoordinate2DMake(-27.3696691,-70.3333776);
point1.title = @"Chañarcillo 760";
point1.subtitle = @"$1.000";
MKPointAnnotation * point4 = [[MKPointAnnotation alloc] init];
point2.coordinate = CLLocationCoordinate2DMake(-27.3695798,-70.3336009);
point2.title = @"Chañarcillo 730";
point2.subtitle = @"$800";
[self.mapview showAnnotations:@[point1, point2, point3, point4] animated:YES];
Hola Juanpe. Me ha gustado tu entrada pero se me presenta alguna duda.
He visto que se implementa un tipo de overlays llamado MKTileOverlays que va muy bien por ejemplo para mostrar una overlay de openstreetmap.
Estoy accediendo a unos datos de un servidor WMS en donde me pasa una imagen dependiendo mi lat/lng. He estado investigando con el MKOverlayRenderer pero no consigo salir de las dudas. ¿Sabes como podría hacer que apareciera un overlay encima del mapa con la imagen que obtengo de dicho WMS? La imagen sería del mismo tamaño que el mismo mapa y se actualizaría cada vez que voy moviendo el mapa, claro.
Muchas gracias.
El metodo «_drawRoutePolyline» en donde lo podría analizar? muchas gracias por tu ayuda.
Puedes verlo en el proyecto de ejemplo del repositorio.
En el siguiente enlace, exactamente línea 315:
https://github.com/Juanpe/MapKitSample_iOS7/blob/master/MapKit%20Sample/ViewController.m
Que tal!
baje el demo pero no me muestra nada, no sabes que pueda ser?
Buenas Arturo,
Acabo de comprobar el código y me ha funcionado. ¿Qué dispositivo y que versión de iOS tienes?
Hola me parece muy bueno tu tutorial, sin embargo tengo una duda como puedo extraer los parámetros ( nombre, descripcion imagen etc) de una mkpointanotation para poder ponerlos en una Label en otra vista algo asi como los mapas cuando le das click a un pin y te pasa a otra vista ojala me puedas ayudar gracias
Excelente, queria saber si conocen algo, algun sitio o algun sistema que pueda cargar direcciones en una web mia y que eso se consuma desde un mapa dentro de una app con mapa de este tipo???
saludos
Me coges un poco fuera de juego, pero que yo sepa los mapas de Apple no permiten aún el poder extraer un iframe interactivo de un trozo de mapa, que me corrijan si me equivoco.
De todas formas para insertar en tu web un mapa yo usaría Google Maps, es muy fácil de integrar y personalizar.
Saludos
Gran artículo Juanpe, muy claros y concisos los ejemplos. Muchas gracias por compartirlo y hacer este genial resumen de novedades, alguna me hubiera venido genial en IOS6! :-).
Un saludo
Pedro
Yo se a más de uno que lo de poder pintar rutas sin ningún recurso de terceros le hubiera salvado mas de un día de código.
Gran artículo.
Gracias señores! La verdad es que el pintar la ruta y el centrar el mapa son las funcionalidades de iOS 7 que me parecen más útiles, sobre todo por lo que estáis comentando, que en iOS 6 hubiese pagado por tenerlo 🙂