Chaque année, Apple propose une nouvelle version d’iOS qui apporte son lot de nouveautés plus ou moins mises en avant. Parmi ces nouveautés, il y en a une que je souhaite mettre à l’honneur, et celle-ci concerne les cartes !

En effet, cela faisait longtemps que le SDK MapKit, qui gère les composants de la carte native Apple, n’avait pas évolué de façon conséquente pour les développeurs. Heureusement, avec iOS 11, Apple rattrape son retard, et c’est au travers d’une application Xamarin.Forms (où seulement les parties partagées et iOS seront présentées) que je vais vous montrer ces évolutions.

Pour cet article, j’ai choisi de m’inspirer du projet exemple présenté à la WWDC 2017 et repris par Xamarin pour présenter les nouveautés iOS 11 pour Xamarin natif. Vous pourrez trouver les sources du projet original sur GitHub.

Un peu de théorique : qu’est ce qui change avec MapKit sur iOS 11 ?

Avant de nous plonger dans du code, faisons un point rapide sur ce qui a été annoncé cette année.

L’évolution des contrôles natifs : boussole, échelle dynamique et bouton de suivi

Ces contrôles ne sont pas nouveaux, cependant avec iO11 leur utilisation est maintenant plus flexible. Premièrement, la position de chacun de ces éléments sur la carte est libre. On peut même décider de placer la boussole dans la barre de navigation. L’autre point qui rend ces contrôles plus flexibles est le mode d’affichage. Celui-ci peut désormais être intelligent ! Au lieu de cacher ou d’afficher systématiquement ces éléments, il est possible de jouer sur la visibilité en fonction de la situation. Ce mode de visibilité est le mode dit “adaptif”. Par exemple, lorsque l’utilisateur tourne le téléphone et que celui-ci n’est plus orienté vers le nord, la boussole s’affiche. Dans ce mode l’échelle des distances est quant à elle visible lorsque l’utilisateur zoom ou dézoom la carte.

Ces nouveautés peuvent paraître anodines, mais cela permet de gagner en espace sur la carte. En effet, cette nouvelle map proposée par Apple s’inscrit dans une logique de lisibilité optimale et d’interface épurée.

Les annotations font peau neuve

iOS 11, MapKit et Xamarin.Forms

On se souvient de l’application Plans (ou des applications iOS tierces utilisant MapKit) et des épingles rouge par défaut pour représenter un point sur la carte. Comme le prouve l’image ci-dessus, ce n’était pas vraiment moderne… Heureusement la fin de ces vilaines épingles est arrivée ! Les équipes d’Apple proposent aujourd’hui un tout nouveau design pour ce qui est communément appelé une annotation (voir ci-dessous). Beaucoup plus moderne, facilement customisable et dotée d’animations, il ne fait aucun doute que cette nouveauté apportera un vent de fraicheur sur toutes les applications iOS possédant une carte !

new-annotation-mapkit-iOS11-Xamarin

On peut également noter que MapKit pour iOS 11 est intelligent puisqu’il est capable de contrôler la visibilité des textes sur la carte et cacher ceux-ci si une annotation est placée “au-dessus”. Encore une fois, on retrouve le souci de lisibilité sur ce contrôle graphique.

La feature waouh : un “clustering” des annotations moderne, animé, et natif !

Si le clustering est déjà bien connu et est très utilisé sur les applications de cartes, il faut savoir que cette fonctionnalité n’était jusqu’à présent pas gérée par MapKit. Mais comme Apple a décidé de s’attaquer au rafraichissement de MapKit, c’était l’occasion d’intégrer le clustering !

Un cluster possède par défaut un design semblable à une annotation, avec le nombre d’annotations regroupées. Il est également possible de customiser totalement le cluster, tout comme l’annotation. Cette opération se fera dans le code, en héritant de la classe MKAnnotationView plutôt que de MKMarkerAnnotationView.

Les annotations peuvent avoir un ou plusieurs identifiants de clustering, selon que l’on souhaite pouvoir les regrouper tous ensemble ou faire des catégories.

Comment la gestion du clustering fonctionne ? Apple se base simplement sur un système de collision entre les annotations affichées. La mise en place est relativement simple, et nous allons justement voir ça tout de suite !

Place à la pratique : intégration d’une Map iOS dans un projet Xamarin.Forms

Le but de ce projet sera d’afficher les arbres remarquables dans la ville de Paris. Les sources viendront du site OpenData de Paris. Voici à quoi doit ressembler le projet à la fin :

Côté Xamarin.Forms (XAML)

Le projet Xamarin.Forms contiendra uniquement une ContentPage. Pour utiliser une Map Xamarin.Forms, il faut dans un premier temps ajouter le paquet NuGet Xamarin.Forms.Map. Dans le fichier XAML, il faut ensuite préciser le namespace ( xmlns:maps=”clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps” ) et il suffit enfin de créer un contrôle Map :

<maps:Map x:Name="MyMap" IsShowingUser="true" MapType="Street" />

On bénéficie ici des propriétés fournies par Xamarin.Forms, telles que “IsShowingUser” qui permet d’afficher la position de l’utilisateur sur la carte, ou encore le type de carte à afficher.

Dans le fichier xaml.cs, il faut récupérer le contenu du fichier json qui contient les données à afficher. Le code ne sera pas détaillé dans cet article, mais il sera disponible sur le Github.

Une fois que les données sont chargées et transformées en une liste d’arbres (avec les coordonnées géographiques), il faut exposer celle-ci sous forme de propriété publique. En effet, elle sera utilisée dans le projet iOS.

Et c’est tout pour le projet Xamarin.Forms ! Pour l’instant si on teste le projet, une map vide s’affiche. Les points correspondant aux arbres ne sont même pas affichés, mais le chapitre suivant va très vite combler ce manque.

Côté iOS

Dans le projet iOS, le point central est du côté d’une classe Renderer. Pour les besoins du projet c’est la page entière qu’il faut customiser, la classe PageWithNativeMapRenderer va donc hériter de PageRenderer. A noter qu’il ne faut pas oublier l’attribut au dessus du namespace, qui ressemble à ceci : [assembly: ExportRenderer(typeof(NomDeMaPageXaml), typeof(NomDeMaClasseRenderer))]

Dans un premier temps c’est l’initialisation de la carte et de tous ses contrôles qui nous intéresse, direction donc la méthode ViewDidLoad.

<pre class="brush: csharp; title: ; notranslate" title="">public override void ViewDidLoad()
{
    base.ViewDidLoad();

    var mapView = MapView;

    if (mapView == null)
        return;

    mapView.GetViewForAnnotation = GetViewForAnnotation;

    mapView.Register(typeof(RemarkableTreeView), "marker");
    mapView.Register(typeof(ClusterView), "cluster");

    mapView.AddAnnotations(FromTreesModelsToAnnotationsTrees(_trees));

    var button = MKUserTrackingButton.FromMapView(mapView);
    button.Layer.BackgroundColor = UIColor.FromRGBA(255, 255, 255, 80).CGColor;
    button.Layer.BorderColor = UIColor.White.CGColor;
    button.Layer.BorderWidth = 1;
    button.Layer.CornerRadius = 5;
    button.TranslatesAutoresizingMaskIntoConstraints = false;
    mapView.AddSubview(button);
}

Dans l’ordre d’exécution de cette méthode, la MapView est récupérée (grâce à un parcours de l’arbre visuel) et stockée. Ensuite la gestion des vues pour les annotations est déléguée à la méthode GetViewForAnnotation. Vient dans un troisième temps l’enregistrement des vues pour les annotations et les clusters. Enfin les annotations sont ajoutées à la carte grâce à la liste des arbres. En bonus, le bouton de suivi d’utilisateur est ajouté à la vue, à une position définie à la main.

Petite parenthèse :

Normalement, la promesse d’Apple avec MapKit pour iOS 11 est de simplifier au maximum l’implémentation du contrôle de carte.
Il faut savoir que la gestion des annotations et clusters est relativement équivalente à la gestion des cellules d’une TableView, puisqu’elle se base sur le principe de “dequeue”. Tout comme pour une table, il faudrait donc définir pour chaque annotation la classe qui correspond.

Les équipes d’Apple ont voulu simplifier l’utilisation en passant par le mapView.Register(typeof(ClusterView), “cluster”). Dans un monde idéal, il serait donc possible de supprimer cette ligne : mapView.GetViewForAnnotation = GetViewForAnnotation (et la méthode associée). Le problème est qu’un bug s’est glissé dans MapKit, qui rend le “dequeue” automatique parfois impossible, et fait planter l’application. Ouch. Pour l’instant aucun fix n’est proposé par Apple, mais il est heureusement possible de contourner le problème.

C’est d’ailleurs pour régler ce problème que la méthode déléguée GetViewForAnnotation est utilisée. Celle-ci va servir de plan B au cas où le DequeueReusableAnnotation, qui est normalement fait automatiquement, renvoie une annotation null (ce qui dans un monde idéal ne devrait pas arriver). Voici le code :

<pre class="brush: csharp; title: ; notranslate" title="">private MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation)
{
    if (annotation is RemarkableTree)
    {
        var marker = annotation as RemarkableTree;

        var view = mapView.DequeueReusableAnnotation(MKMapViewDefault.AnnotationViewReuseIdentifier) as RemarkableTreeView;
        if (view == null)
        {
            view = new RemarkableTreeView(marker, MKMapViewDefault.AnnotationViewReuseIdentifier);
        }
        return view;
    }
    else if (annotation is MKClusterAnnotation)
    {
        var cluster = annotation as MKClusterAnnotation;

        var view = mapView.DequeueReusableAnnotation(MKMapViewDefault.ClusterAnnotationViewReuseIdentifier) as ClusterView;
        if (view == null)
        {
            view = new ClusterView(cluster, MKMapViewDefault.ClusterAnnotationViewReuseIdentifier);
        }
        return view;
    }
    else if (annotation != null)
    {
        var unwrappedAnnotation = MKAnnotationWrapperExtensions.UnwrapClusterAnnotation(annotation);

        return GetViewForAnnotation(mapView, unwrappedAnnotation);
    }
    return null;
}

Comme on peut le voir chaque type d’annotation est géré, cela va permettre d’éviter qu’un cluster ou qu’une annotation ne soit null et fasse crasher l’application.

On fait quoi pour les annotations ?

Enfin, il reste à se pencher sur les classes qui vont représenter les annotations et clusters. Celles-ci sont au nombre de trois :

  • La première représentera le modèle lié à une annotation. Ici, ce sera un arbre de Paris, avec pour nom RemarkableTree. C’est dans cette classe que seront définies les coordonnées géographiques du point.
  • RemarkableTreeView modélisera quant à elle la vue liée à l’annotation. C’est dans cette classe que l’annotation pourra être customisée, avec une image, un texte, une couleur de fond…
  • Enfin la troisième classe représente la vue du cluster et sera nommée ClusterView. Tout comme pour un RemarkableTreeView, la vue du cluster peut-être customisée. Cet exemple reprend le design du sample Tandm proposé par Apple ou Xamarin, c’est à dire un cercle de couleur avec en son centre sur fond blanc le nombre d’annotations contenues.

Enfin, il est important de souligner que la personnalisation des clusters peut même aller jusqu’au mode de collision. Dans le cas de ce projet exemple, les clusters sont sous forme de cercle, leur mode de collision sera donc le cercle. Cette forme de collision, ainsi que le rectangle sont disponibles par défaut, mais il est possible de réaliser son propre mode de collision. Pour plus d’informations sur cette fonctionnalité avancée, je vous suggère de vous référer à la conférence de la WWDC 2017 sur les nouveautés MapKit.

Réflexion autour de cette solution : pourquoi ne pas utiliser un MapRenderer ?

Lorsque j’ai démarré ce projet, j’ai évidemment tenté d’utiliser une Map Xamarin.Forms et de customiser celle-ci pour iOS avec un MapRenderer. Malheureusement après plusieurs heures de casse tête à essayer de faire marcher le clustering, j’ai dû m’avouer vaincue. En effet, il semblerait que l’enregistrement des Annotations et ClusterAnnotations doive se faire dans le ViewDidLoad d’une page. Au final, le plus simple restait de customiser directement la page. Cette solution permet d’avoir accès aux évènements iOS dont la carte a besoin.
J’ai toutefois fait le choix de conserver un contrôle Map Xamarin.Forms afin de garder un maximum d’éléments du côté code partagé. Dans le cas d’une UI un peu complexe on garde ainsi l’avantage de la structure du XAML, et cela facilite le placement des éléments d’interface graphique. Il faut ensuite récupérer la Map sous forme de contrôle natif côté iOS. Pour cela, rien de mieux qu’un parcours de l’arbre visuel !

Pour finir sur MapKit et Xamarin.Forms

Nous avons vu dans cet article comment tirer parti des nouveautés MapKit pour iOS 11 pour une application Xamarin.Forms. Le code pour une application Xamarin native ressemble beaucoup à ce qui est défini dans le Renderer. Même si le clustering est géré nativement par iOS, attention toutefois à ne pas abuser concernant le nombre de points que l’on souhaite afficher ! Au delà de 200 points environ, malgré le clustering la carte subit quelques ralentissements/freeze. Dans un cas comme celui-là, il faut réfléchir à par exemple charger les annotations dans un certain rayon.

Si vous souhaitez visualiser le code plus en détail, vous pouvez le retrouver sur mon Github.

A vos claviers !

Livre Blanc Cell'insight 5 Xamarin