RubyMotion - utilisation de MapKit

Publié le 19 février 2014 par Nicolas Cavigneaux | mobile

Cet article est publié sous licence CC BY-NC-SA

Dans cet article à propos de RubyMotion, nous allons voir comment mettre en place un système de géo-localisation et l’affichage d’une carte.

Maitriser ces deux outils élargit largement la palette des possibilités de fonctionnalités de vos applications. Il sera possible de lier des évènements à des lieux et de les afficher sur une carte ou encore d’utiliser la localisation actuelle de l’utilisateur pour lui proposer des services à proximité.

Création du projet

Commençons donc par créer un nouveau projet :

$ motion create mapkit                                                                                                                       [13:18:02]
Create mapkit
Create mapkit/.gitignore
Create mapkit/app/app_delegate.rb
Create mapkit/Gemfile
Create mapkit/Rakefile
Create mapkit/resources/Default-568h@2x.png
Create mapkit/spec/main_spec.rb

Nous allons définir un contrôleur principal qui nous servira à gérer l’unique vue de notre application :

app/app_delegate.rb

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    mapViewController = MapViewController.alloc.init
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = mapViewController
    @window.makeKeyAndVisible
    true
  end
end

app/controllers/map_view_controller.rb

class MapViewController < UIViewController

  def loadView
    self.view = UIView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    self.view.backgroundColor = UIColor.whiteColor
  end
end

Si vous lancez l’application, vous obtiendrez une fenêtre sur fond blanc. Nous avons posé les bases et allons pouvoir entrer dans le vif du sujet.

Ajout d’une carte par défaut

Tout d’abord, nous allons commencer par afficher une carte pointée sur une localisation par défaut. Nous prendrons ici l’exemple des bureaux de Synbioz.

Pour pouvoir afficher une carte et utiliser la géo-localisation nous allons devoir préciser que nous souhaitons utiliser les frameworks adéquats. Ceci se fait dans la partie configuration du Rakefile :

app.frameworks = ['CoreLocation', 'MapKit']

CoreLocation est le framework qui permet d’accéder à la géo-localisation / positionnement de l’appareil. MapKit sert quant à lui à embarquer des cartes dans l’application et à interagir avec. Vous pourrez notamment ajouter des marqueurs sur la carte, géo-coder une adresse et bien plus encore.

Affichage de la carte

Commençons simplement par afficher la carte de base. Nous allons utiliser toute la largeur de l’écran, concernant la hauteur nous allons laisser 100px pour notre champ texte à venir qui servira à changer d’adresse. Voici donc à quoi doit ressembler app/controllers/map_view_controller.rb :

class MapViewController < UIViewController

  def loadView
    self.view = UIView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    self.view.backgroundColor = UIColor.whiteColor

    @mapView = mapView
    self.view.addSubview @mapView
  end

  private

  def mapView
    topMargin = 100
    width     = UIScreen.mainScreen.bounds.size.width
    height    = UIScreen.mainScreen.bounds.size.height - topMargin

    view = MKMapView.alloc.initWithFrame([[0, topMargin], [width, height]])
    view.mapType = ::MKMapTypeStandard
    view
  end

end

Nous définissons donc une méthode privée mapView qui va nous permettre d’instancier notre MKMapView et de définir sa taille. Nous utilisons ensuite le retour de cette méthode pour ajouter une sous-vue à notre vue principale. Voici le résultat :

Carte par défaut

Comme vous l’aurez remarqué, il est possible de définir le type de carte à afficher. Nous avons ici utilisé le type MKMapTypeStandard. Trois types sont disponibles :

  • MKMapTypeStandard : affichage des rues, des routes et de leurs noms
  • MKMapTypeSatellite : image satellite
  • MKMapTypeHybrid : mix des deux premiers types

Définition d’un emplacement personnalisé

Maintenant que nous avons notre carte, nous souhaitons la pointer par défaut sur les bureaux de Synbioz et idéalement augmenter le niveau de zoom. Pour ce faire, nous allons modifier la méthode mapView pour préciser les coordonnées à afficher au centre ainsi que le niveau de zoom :

def mapView
  topMargin = 100
  width     = UIScreen.mainScreen.bounds.size.width
  height    = UIScreen.mainScreen.bounds.size.height - topMargin

  view = MKMapView.alloc.initWithFrame([[0, topMargin], [width, height]])
  view.mapType = ::MKMapTypeStandard

  coordinates = CLLocationCoordinate2DMake(50.6308091, 3.0210861)
  region      = MKCoordinateRegionMake(coordinates, MKCoordinateSpanMake(0.05, 0.05))
  view.setRegion region

  view
end

Nous avons donc créé les coordonnées à l’aide de CLLocationCoordinate2DMake qui est une structure permettant de définir une latitude et une longitude. Suite à ça nous créons une région à afficher à l’aide de MKCoordinateRegionMake qui prend deux paramètres. Le premier est le centre de la région, il attend une structure CLLocationCoordinate2DMake, le deuxième est une structure MKCoordinateSpanMake qui définie le delta de coordonnées visibles entre le centre et le bord de la carte. Pour simplifier, plus ces valeurs sont petites, plus le zoom est grand et inversement.

Ajout d’un marqueur

Pour finaliser cette carte basique, il serait bienvenu d’ajouter un marqueur sur ces coordonnées pour que l’emplacement soit bien visible sur la carte. Ajoutons donc cette fonctionnalité en modifiant notre méthode pour appeler addAnnotation sur notre MKMapView. La classe annotation attend une instance d’une classe qui implémente le protocole MKAnnotation. Les méthodes requises par ce protocole sont :

  • coordinate : Retourne un CLLocationCoordinate
  • title : retourne une NSString qui servira comme titre
  • subtitle : retourne une NSString qui servira comme sous-titre

Vous devez absolument créer une classe et ses méthodes, passer par un Struct ou une classe et des attr_reader causera un plantage. Je ne saurai pas dire si c’est un bug de RubyMotion ou autre chose mais j’ai en tout cas constaté ce comportement. Voici donc notre classe d’annotations :

app/models/annotation.rb

class Annotation
  def initWithCoordinate(coordinate, title: title, subtitle: subtitle)
    @coordinate = coordinate
    @title      = title
    @subtitle   = subtitle

    self
  end

  def coordinate
    @coordinate
  end

  def title
    @title
  end

  def subtitle
    @subtitle
  end
end

Cette classe est tout à fait classique et ne présente aucune difficulté de compréhension. Nous pouvons maintenant l’utiliser dans notre méthode mapView pour ajouter l’annotation :

app/controllers/map_view_controller.rb

def mapView
  topMargin = 100
  width     = UIScreen.mainScreen.bounds.size.width
  height    = UIScreen.mainScreen.bounds.size.height - topMargin

  view = MKMapView.alloc.initWithFrame([[0, topMargin], [width, height]])
  view.mapType = ::MKMapTypeStandard

  coordinates = CLLocationCoordinate2DMake(50.6308091, 3.0210861)
  region      = MKCoordinateRegionMake(coordinates, MKCoordinateSpanMake(0.05, 0.05))
  view.setRegion region

  synbioz = Annotation.alloc.initWithCoordinate(coordinates, title: "Synbioz", subtitle: "2, rue Hegel, 59000, Lille, France")
  view.addAnnotation synbioz

  view
end

Nous créons donc une instance de notre Annotation en reprenant les coordonnées définies plus haut puis en choississant un titre et un sous-titre, il nous suffit ensuite de passer cette instance à la méthode addAnnotation. Voici le résultat :

Carte pour Synbioz

Choix du type de carte

Nous aimerions maintenant pouvoir modifier le type de carte affiché à la volée. Pour cela, le plus simple est de mettre en place un segmentedControl qui nous permettra de passer d’un affichage à l’autre sur un simple clic.

Nous allons donc créer une nouvelle méthode privée qui permet de générer cette vue puis nous l’insérerons en tant que sous-vue :

def segmentedControl
  segmentedControl = UISegmentedControl.alloc.initWithItems(['Standard', 'Satellite', 'Hybride'])
  segmentedControl.frame = [[20, UIScreen.mainScreen.bounds.size.height - 60], [280,40]]
  segmentedControl.selectedSegmentIndex = 0
  segmentedControl.addTarget(self,
                            action:"switchMapType:",
                            forControlEvents:UIControlEventValueChanged)
  segmentedControl
end

Cette méthode crée une instance de UISegmentedControl en précisant les éléments à y afficher. Ensuite, on définie la taille et la position de la frame. On marque le premier segment comme étant actif. Finalement, on définit la méthode à appeler lorsque la valeur sélectionnée change et on retourne la vue.

Il ne reste donc plus qu’à ajouter cette vue en tant que sous-vue à la fin de la méthode loadView :

self.view.addSubview(segmentedControl)

Si vous lancez l’application, vous verrez apparaître le nouveau controle mais il nous reste encore à écrire la méthode appelée lors de la sélection d’un autre mode d’affichage :

def switchMapType(segmentedControl)
  @mapView.mapType = case segmentedControl.selectedSegmentIndex
    when 0 then MKMapTypeStandard
    when 1 then MKMapTypeSatellite
    when 2 then MKMapTypeHybrid
  end
end

En fonction de l’index du segment sélectionné on va donc affecter le mapType désiré à notre carte.

Voici le résultat obtenu :

Carte en mode hybride

Modification de la localisation

Nous allons maintenant ajouter un champ texte pour pouvoir saisir une adresse et faire en sorte qu’elle soit utilisée pour mettre à jour la carte.

app/controllers/map_view_controller.rb

def locationField
  field = UITextField.alloc.initWithFrame([[10,30],[UIScreen.mainScreen.bounds.size.width-20,30]])
  field.borderStyle = UITextBorderStyleRoundedRect

  field
end

On ajoute une méthode qui va nous permettre de générer notre champ texte, en haut de l’écran, avec une bordure pour qu’il soit plus visible. Il faut donc ensuite l’ajouter en tant que sous-vue dans la méthode loadView :

@locationField = locationField
@locationField.delegate = self

self.view.addSubview @locationField

Rien de particulier ici, on pense à déléguer la gestion de la vue à notre contrôleur courant pour pouvoir réagir lorsque l’utilisateur appuira sur la touche “entrée”.

Lorsque l’utilisateur va valider sa saisie, nous allons faire en sorte de masquer le clavier puis nous lancerons un géo-encodage de l’adresse fournie. Si au moins un résultat nous est retourné, nous mettrons à jour la position de la carte :

app/controllers/map_view_controller.rb

def textFieldShouldReturn (textField)
  @locationField.resignFirstResponder

  geocoder = CLGeocoder.alloc.init
  geocoder.geocodeAddressString @locationField.text,
                                completionHandler: lambda { |places, error|
                                  if places.any?
                                    coordinates = places.first.location.coordinate
                                    region = MKCoordinateRegionMake(coordinates, MKCoordinateSpanMake(0.05, 0.05))
                                    @mapView.setRegion region
                                  end
                                }
end

On masque donc le clavier dès que l’utilisateur valide puis on instancie CLGeocoder qui va nous permettre de transformer notre adresse en coordonées. Cette opération est asynchrone c’est pourquoi on passe par un handler et un lambda.

Le premier paramètre de l’appel à la méthode geocodeAddressString est une chaîne de caractères représentant l’adresse. Ici on récupére simplement le texte de notre champ.

Concernant l’handler, deux paramètres lui sont passés en fin de requête. Le premier représente un tableau des coordonnées correspondants à notre adresse, le deuxième représente l’erreur si la requête n’a pas pu aboutir.

Dans notre exemple on vérifie qu’il y a bien au moins un résultat, si c’est le cas, on récupére les coordonnées du premier résultat puis on s’en sert pour créer une nouvelle région et l’affecter à notre instance de mapView.

La carte est donc déplacée vers cette nouvelle adresse. Plutôt simple et efficace, n’est-ce pas ?

Détection de la localisation

Il est bien évidemment possible de détecter la position actuelle de l’appareil si toutefois son utilisateur nous y autorise. Il pourrait être intéressant de voir comment récupérer cette information pour l’utiliser pour la position initiale de la carte. Mettons donc cela en place.

La première chose à faire est de voir si l’appareil possède un système de géo-localisation et si nous sommes autorisés à l’utiliser. Nous allons donc créer une méthode qui fait cette vérification. Cette méthode sera appelée dans loadView :

def userLocation
  if (CLLocationManager.locationServicesEnabled)
    @location_manager = CLLocationManager.alloc.init
    @location_manager.desiredAccuracy = KCLLocationAccuracyKilometer
    @location_manager.delegate = self
    @location_manager.purpose = "Permet d'initialiser la carte sur votre position"
    @location_manager.startUpdatingLocation
  end
end

On vérifie tout d’abord que le service de localisation est disponible, s’il l’est on l’instancie. On définit ensuite la précision de localisation, on pourrait augmenter cette précision mais cela demanderai plus de ressources et d’énergie. Tout dépendra ici des besoins de votre application. On délègue ensuite à notre contrôleur pour pouvoir réagir aux changements de localisation.

La méthode purpose permet de mettre un descriptif qui sera affiché à l’utilisateur lors de la demande d’autorisation :

Demande d'autorisation

Finalement, on commence le tracking.

Cette méthode est à appeler à la fin de notre méthode loadView.

Nous pouvons maintenant mettre en place deux callbacks qui seront appelés lorsque la position est mise-à-jour. Cette mise-à-jour peut fonctionner comme produire une erreur. Chaque cas possède son callback dédié. Nous allons donc mettre à jour la position sur la carte à chaque fois que possible :

def locationManager(manager, didUpdateToLocation:newLocation, fromLocation:oldLocation)
  if newLocation != oldLocation
    region = MKCoordinateRegionMake(newLocation.coordinate, MKCoordinateSpanMake(0.05, 0.05))
    @mapView.setRegion region
  end
end

def locationManager(manager, didFailWithError:error)
  puts "error location user"
end

La méthode gérant les erreurs ne présente aucun intérêt dans cette implémentation. Penchons nous donc sur la version dédiée à une mise à jour de la position ayant eu lieu avec succès.

Je prend ici le soin de vérifier que la nouvelle position est différente de l’ancienne pour éviter les traitements inutiles. Si les positions sont différentes, comme vu auparavant, nous créons une nouvelle région utilisant ces coordonnées puis nous l’affectons à la carte. Votre carte vous suivra donc en temps réel.

Une fois encore, une fonctionnalité très puissante et utile mais pourtant très simple à mettre en place.

Conclusion

Nous avons pu mettre en place une carte, un marqueur, du geo-coding mais aussi de la géo-localisation. Toutes ces fonctionnalités sont très accessibles et vous apporterons de nombreuses nouvelles possibilités en terme d’ergonomie et de fonctionnalités. Il ne faut donc pas vous en priver.

Vous trouverez l’ensemble du code d’exemple cet article, découpé en commits sur GitHub.


L’équipe Synbioz.

Libres d’être ensemble.