De l'utilisation du Map Kit - Intégration de cartes Google Maps - Initiation et astuces
Hello !
Mon application utilise assez intensivement les cartes Google proposées dans le Map Kit d'iOS (enfin, je l'utilise à 2 endroits )
J'ai déjà passé pas mal d'heures sur le sujet.
Certaines heures très satisfaisantes par les fonctionnalités que le Map Kit peut apporter à une application.
D'autres heures moins glorieuses à tenter de comprendre pourquoi mon appli crash soudainement sans autres explications qu'un vulgaire (et non moins grossier) EXC_BAD_ACCESS
Je vous propose donc un petit tutoriel qui compile pas mal de conseils et d'astuces pour créer des cartes de base avec pas mal de fonctionnalités.
Synthèse des classes mises à disposition par le Map Kit
Il existe bien sûr d'autres classes mais celles-ci vont déjà pas mal nous occuper !
Quelques notions
Principe de fonctionnement des delegate et callback méthodes
La plupart des classes présentées au précédent chapitre fonctionnent sur le principe du delegate/call back. Pourquoi ? Parce que la plupart des actions effectuées par les vues ou les objets sont asynchrones. L'objet va devoir aller solliciter un serveur externe (Google) avant de pouvoir renvoyer une réponse.
Dans l'ordre :
- on instancie une classe dans un objet,
- on indique à l'objet son “delegate”,
- on lance les actions mises à disposition par l'objet,
- le delegate implémente les fonctions de callback qui seront appelées par l'objet lorsqu'il aura fait ses actions.
Les fonctions de callback sont “déclarées” dans le protocol associé.
Exemple concret avec la classe de base : MKMapView
- instanciation de la classe :
MKMapView * map=[[MKMapView alloc] initWithFrame:aFrame];
- affecter le delegate :
map.delegate=self; /* par exemple... */
- on lance une action :
[map setRegion:maRegion animated:YES]
- on attend les retours à travers les méthodes (que j'appelle “méthodes de callback”) définies dans le delegate (dans notre exemple, self, c'est à dire, l'objet qui a créé la map). Ces fonctions sont déclarées dans le protocole MKMapViewDelegate.
Voilà pour le principe de base qui n'est pas exclusivement réservé au Map Kit d'ailleurs. C'est un principe très utilisé en Objective-C.
Geocoding
- Le geocoding (Forward Geocoding in english) est une mécanique qui permet de transformer une adresse en coordonnées géographiques (latitude-longitude).
- Le geocoding inverse (Reverse Geocoding in english) est la mécanique inverse : “J'ai une latitude/longitude, à quelle adresse je me trouve ?”
Google fournit les 2 mécaniques mais Apple n'en propose qu'une dans son Map Kit (le Reverse). Si vous voulez faire du Forward Geocoding, passez votre chemin ! Dommage…
MKMapView - on se lance
Le b.a.ba
Alors voilà, vous voulez afficher une map dans une vue de votre application. C'est partit ! Il vous faut :
- Ajouter le framework MapKit.framework dans le groupe des frameworks liés,
- Importer le header correspondant dans le controleur de la vue qui va créer la map :
#import <MapKit/MapKit.h>
- Créer votre MKMapView. Deux solutions, soit via IB, soit par programme.
- Sous IB : déplacez simplement l'objet MKMapView sur la vue qui vous intéresse (ou comme vue autonome).
- Par programme :
MKMapView * map=[[MKMapView alloc] initWithFrame:<uneFrame>]; [votreVue addSubviw:map];
- relier votre MKMapView à son delegate :
- soit en la reliant par outlet sous IB,
- soit en l'affectant par code
maMapView.delegate=monDelegate;
- déclarer que le delegate répond bien au protocole MKMapViewDelegate
- dans le header du delegate, on doit avoir
@interface ClasseDeMonDelegate : UIViewController (par ex) <MKMapViewDelegate>
- et enfin, définir dans ce delegate les méthodes du protocole. 2 méthodes de base :
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView; - (void)mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error;
Et voilà ! Notre map s'affiche, elle va télécharger ses “tuiles” comme une grande, l'utilisateur peut se déplacer dedans, zoomer, dézoomer, la map va s'adapter en téléchargeant les tuiles correspondantes ; et à chaque fois, prévenir notre delegate que le chargement est terminé ou qu'il a échoué.
As simple as that
On a bien sûr tout un arsenal de fonctions au niveau du delegate qui nous permettent d'être informés de tout ce qu'il se passe : l'utilisateur se déplace dans la map, il zoom, sa position actuelle (réelle !) a changé, etc…
On pourra profiter de mapViewWillStartLoadingMap et DidFinishLoadingMap pour gérer un indicateur de chargement.
La map est dotée de quelques propriétés qui permettent de la paramétrées. Un exemple simple : le type de carte ⇒
map.mapType=MKMapTypeHybrid /* affichera une carte satellite + plan */
Déplaçons et "zoomons"
Au chargement de votre map précédemment créée, on va afficher la bonne vieille carte du monde Google. Vous voudrez rapidement (si si, je vous jure !) afficher une région plus précise que le monde entier… Encore une fois, c'est plutot simple. On va utiliser les méthodes d'instance de notre map :
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated; - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated;
- la première permet de centrer la carte sur les coordonnées qu'on lui indique, sans changer le zoom.
- la seconde permet la même chose mais en indiquant un niveau de zoom.
MKCoordinateRegion est ton amie puisque c'est une structure de données faite pour ça !
typedef struct { CLLocationCoordinate2D center; MKCoordinateSpan span; } MKCoordinateRegion;

Un "zoli" effet de prise de hauteur puis plongeon
On a un bel exemple de ce genre d'animation dans un exemple Apple. On se fait 2 fonctions dans le controller qui détient notre map :
- (void)zoomToWorld:(NSDictionary *)location MKCoordinateRegion current = map.region; MKCoordinateRegion zoomOut = { { (current.center.latitude + [[location valueForKey:@"lat"] doubleValue])/2.0 , (current.center.longitude + [[location valueForKey:@"lat"] doubleValue])/2.0 }, {90, 90} }; [map setRegion:zoomOut animated:YES]; } - (void)zoomToLocation:(NSDictionary *)location { CLLocationCoordinate2D coord; coord.latitude=[[location objectForKey:@"lat"] doubleValue]; coord.longitude=[[location objectForKey:@"lon"] doubleValue]; MKCoordinateRegion region=MKCoordinateRegionMakeWithDistance(coord,1000,1000); [map setRegion:region animated:YES]; }
Ensuite, il suffit d'appeler ces 2 fonctions de manière décalée dans le temps via par exemple :
[self performSelector:@selector(zoomToWorld:) withObject:coordinate afterDelay:0.1]; [self performSelector:@selector(zoomToLocation:) withObject:coordinate afterDelay:1.5];
En effet, si la map est en train de charger ses “tuiles” (chose qu'on ne maitrise pas) et que vous la désallouez, sachez que vous courrez au EXC_BAD_ACCESS… Encore une fois, si vous avez allouez via IB, vous ne courrez pas ce genre de risque. Puisqu'une désallocation de la map associée signifie presque certainement que vous désallouez le reste…
Attention également quand vous passez en background… des messages genre “void -[MKTileCache synchronize](MKTileCache*, objc_selector*) called while in background!” ne sont pas de bon augure !
Une carte sans annotation, c'est un peu comme un mac sans pomme...
Pour les annotations, on suivra à peu près le même principe :
- on crée une annotation,
- on l'ajoute à notre MKMapView,
- on définit les méthodes qui vont nous permettre de customiser notre annotation dans le delegate de la mapView (et qui seront appelées par notification (ou callback))
MKAnnotation protocol
Il n'y a pas de classe MKAnnotation. Pour créer une annotation, on va donc créer un objet quelconque dérivé de NSObject par exemple et on va juste le rendre conforme au protocole MKAnnotation.
Pour être conforme, on va déclarer les propriétés suivantes :
- les coordonnées (CLLocationCoordinate2D) ⇒ coordinate
- un titre ⇒ title,
- un sous titre ⇒ subtitle.
Seules les coordonnées sont obligatoires. On définira les setter et getter idoines.
Si vous voulez stocker d'autres infos dans une annotation (et vous le voudrez surement), no problemo.
On prendra soin de déclarer que notre classe est conforme au protocole dans son header en ajoutant <MKAnnotation>.
MKAnnotationView
Bon maintenant qu'on a notre (ou nos) annotation(s), y a plus qu'à les afficher.
Ca se fait grosso modo en 2 étapes :
- on les ajoute simplement à notre map,
- on définit les fonctions de callback que la map va appeler à travers son delegate pour gérer les paramètres d'affichage.
Allons-y !
/* Ici, on va ajouter notre annotation */ MaClasseConformeAMKAnnotationProtocol * monAnnotation=[[MaClasseConformeAMKAnnotationProtocol alloc] init]; monAnnotation.coordinate={latitude,longitude}; /* pratique comme écriture... */ monAnnotation.title=@"Exemple d'annotation"; [maMap addAnnotation:monAnnotation]; [monAnnotation release]; /* la map fait un retain sur l'annotation. On peut releaser tranquille */ /* Ajout annotation ok */ /* Ici, on va définir les fonctions de callback que la map va appeler quand elle détecte qu'elle doit afficher l'annotation - ben oui, si l'annotation est en France et que la carte affiche l'espagne, elle va pas afficher l'annotation... */ - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation { /* Afin de ne pas saturer la mémoire, on va utiliser ici un mécanisme de réutilisation des MKAnnotationView */ static NSString *AnnotationViewID = @"annotationViewID"; MKAnnotationView *annotationView =[mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationViewID]; /* si pas en stock, on alloue */ if (annotationView == nil) { annotationView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:AnnotationViewID] autorelease]; /* on pourra utiliser MKPinAnnotationView pour avoir un pin's */ } /* sinon, on recycle */ else { annotationView.annotation = annotation; } /* si on veut customiser notre annotation */ annotationView.image=[UIImage imageNamed:@"CASiteClip.png"]; /* si on veut que notre annotation produise un pop up quand l'utilisateur "tape" dessus */ [annotationView setCanShowCallout:YES]; /* ici, on veut que le pop up dispose d'un bouton à droite... */ UIButton * disclosure=[UIButton buttonWithType:UIButtonTypeCustom]; UIImage * customDisclosure=[UIImage imageNamed:@"CACustomDisclosure"]; disclosure.frame=CGRectMake(0, 0, customDisclosure.size.width, customDisclosure.size.height); [disclosure setImage:customDisclosure forState:UIControlStateNormal]; annotationView.rightCalloutAccessoryView = disclosure; /* on peut décaler l'annotation de qques pixels */ annotationView.centerOffset=unOffset; /* et voilà ! on retourne la toute nouvelle MKAnnotationView à notre map */ return annotationView; } /* Ici, on va définir la fonction que la map appellera quand un utilisateur "tape" sur notre annotation */ - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { /* on dispose de toutes les propriétés de notre MKAnnotationView et de notre MKAnnotation par ex, view.annotation.coordinate.latitude ou view.annotation.title */ }
Voilà notre map parée de sa belle annotation
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView { /* ici, on choisit uniquement la dernière annotation. Mais on pourrait boucler sur toutes... */ if ([mapView annotations]) [mapView selectAnnotation:[[mapView annotations] lastObject] animated:NO]; }
Et si j'en ai plusieurs des annotations !?
Aller, cadeau, une super astuce qui marche du tonnerre : une fonction qui cadre automatiquement la map pour que toutes les annotations soient visibles !
-(void)zoomToFitMapAnnotations:(MKMapView*)mapView { if([mapView.annotations count] == 0) return; CLLocationCoordinate2D topLeftCoord; topLeftCoord.latitude = -90; topLeftCoord.longitude = 180; CLLocationCoordinate2D bottomRightCoord; bottomRightCoord.latitude = 90; bottomRightCoord.longitude = -180; for(MaClassePersoDeMesAnnotationsAMoi * annotation in mapView.annotations) { topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude); topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude); bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude); bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude); } MKCoordinateRegion region; region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5; region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5; region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.2; // Add a little extra space on the sides region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.2; // Add a little extra space on the sides region = [mapView regionThatFits:region]; [mapView setRegion:region animated:YES]; }
A l'envers, à l'endroit...
Le Geocoding se fait à l'endroit (on a une adresse, on veut des coordonnées) ou à l'envers (on a des coordonnées, on veut une adresse).
La Map Kit ne permet de geocoder qu'à l'envers via le MKReverseGeocoder. Mais avant de l'aborder, parlons un peu des…
MKPlacemark
Bon là, on est dans du simple. C'est un objet avec des attributs d'adresse :
- addressDictionary ⇒ en rapport avec Carnet d'@ (adresse d'un contact)
- thoroughfare ⇒ la rue
- subThoroughfare ⇒ la rue si plus d'info
- locality ⇒ ville, village
- subLocality ⇒ précision sur ville, village
- administrativeArea ⇒ je vous laisse voir la doc à partir de maintenant… Non mais
- subAdministrativeArea
- postalCode
- country
- countryCode
On pourrait très bien utiliser une sous-classe de MKPlacemark, y ajouter les propriétés coordinate et title et s'en servir pour une annotation… Par exemple
Mais on va surtout s'en servir avec le…
MKReverseGeocoder
Ta da ! Ca tue comme nom
Vous l'aurez compris, cette classe va permettre de retrouver une adresse à partir de coordonnées. Elle fonctionne comme les autres, c'est à dire qu'on lui demande de faire des trucs et on définit des méthodes de callback pour qu'elle nous renvoie les infos. Un ch'tit exemple :
CLLocationCoordinate2D coordinate; coordinate.latitude=uneLatitude; coordinate.longitude=uneLongitude; MKReverseGeocoder * geocoder=[[MKReverseGeocoder alloc] initWithCoordinate:coordinate]; /* qui sait y qu'il doit prévenir ? */ geocoder.delegate=self; /* Va chercher le chien :-) */ [newSearch start];
Et voilà notre geocoder qui lance sa recherche. Il va nous prévenir grâce à :
/* oh, un placemark !!! */ - (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFindPlacemark:(MKPlacemark *)placemark
ou alors…
/* Aie, ca fait mal... */ - (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error
A vous d'imaginer la suite une fois que vous avez votre placemark !

Et maintenant, que vais je faire...
J'en ai fini avec mon tuto sur le Map Kit. iOS 4 apporte une nouveauté : l'overlay. C'est la possibilité d'ajouter des formes à une map (par exemple des lignes, des cercles, etc). Je ne l'ai pas encore expérimenté. Mais alors, mais alors…
Comment afficher un itinéraire ?!
Allez-vous me dire ?
Avec les overlays sans doute. Mais il y a une autre solution, plus simple à mettre en oeuvre : lancer l'application Plan avec tous ce qu'il faut comme paramètres.
Je vous donne un exemple prémaché qui a fait ses preuves :
/* je suis dans la méthode appelée lorsque l'utilisateur "tape" une annotation */ - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { // Open map application NSString * url=[NSString stringWithFormat:@"http://maps.google.com/maps?daddr=%f,%f&saddr=%f,%f" view.annotation.coordinate.latitude, view.annotation.coordinate.longitude, userLoc.latitude,userLoc.longitude]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
Bon là, je fais mon fainéant car il aurait fallut encoder correctement ma NSString (avec les carctères %, etc d'une URL).
Dans cet exemple, quand l'utilisateur “tape” une annotation, on lance un itinéraire entre sa position et l'annotation dans l'application Plan. Cool non ?
Encore mieux, je vous indique un super lien qui vous permettra de connaitre toutes les astuces de ce système de lancement de Plan : http://mapki.com/wiki/Google_Map_Parameters
Astuces et Synthèse (ou synthèse des astuces...)
- Pas de Forward Geocoding natif… Il faut passer par des web services (ceux de Google par exemple mais il en existe d'autres) pour pouvoir convertir une adresse en coordonnées.
- Pour obtenir une adresse à partir de coordonnées, utilisez le MKReverseGeocoding.
- Préférez créer vos maps sous IB que par programme.
- Toujours mettre le delegate d'une MKMapView=nil avant de faire un release de la map.
- ne cherchez pas de classe MKAnnotation, il n'y en a pas. A la place, définissez une classe perso telle que
@interface MaClasseAnnotation : NSObject <MKAnnotation>
et définissez les propriétés coordinate (obligatoire), title (optionelle) et subtitle (optionnelle).
- Pour faire en sorte qu'une annotation affiche sa pop up sans interaction, on le fait dans mapViewDidFinishLoading avec la méthode selectAnnotation:animated:
- pour afficher un itinéraire, on fait bosser “Plan”
Conclusion
C'est fini ! J'espère que ça vous aidera.
A+
LeChatNoir
