RubyMotion - utilisation de MapKit

Image non disponible Image non disponible

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

Maîtriser 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é.

Cet article est publié avec l'aimable autorisation de Synbioz, l'article original peut être lu sur le blog de Synbioz : RubyMotion - utilisation de MapKit.

Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Création du projet

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

 
Sélectionnez
$ motion create mapkit

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
Sélectionnez
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
Sélectionnez
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.

II. 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éolocalisation, nous allons devoir préciser que nous souhaitons utiliser les frameworks adéquats. Ceci se fait dans la partie configuration du Rakefile :

 
Sélectionnez
app.frameworks = ['CoreLocation', 'MapKit']

CoreLocation est le framework qui permet d'accéder à la géolocalisation / 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éocoder une adresse et bien plus encore.

II-A. 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 :

app/controllers/map_view_controller.rb
Sélectionnez
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 :

Image non disponible

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.

II-B. Définition d'un emplacement personnalisé

Maintenant que nous avons notre carte, nous souhaitons la faire 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 :

 
Sélectionnez
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éfinit 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.

II-C. 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 saurais pas dire si c'est un bogue de RubyMotion ou autre chose mais j'ai en tout cas constaté ce comportement. Voici donc notre classe d'annotations :

app/models/annotation.rb
Sélectionnez
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
Sélectionnez
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 choisissant un titre et un sous-titre, il nous suffit ensuite de passer cette instance à la méthode addAnnotation. Voici le résultat :

Image non disponible

II-D. 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 :

 
Sélectionnez
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éfinit la taille et la position du cadre. 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 :

 
Sélectionnez
self.view.addSubview(segmentedControl)

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

 
Sélectionnez
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 :

Image non disponible

III. 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
Sélectionnez
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 :

 
Sélectionnez
@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 appuiera sur la touche « entrée ».

Lorsque l'utilisateur va valider sa saisie, nous allons masquer le clavier, puis nous lancerons un géocodage 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
Sélectionnez
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 coordonnées. Cette opération est asynchrone, c'est pourquoi on passe par une fonction de rappel 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 la fonction de rappel, deux paramètres lui sont passés en fin de requête. Le premier représente un tableau des coordonnées correspondant à 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 ?

IV. 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éolocalisation 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 :

 
Sélectionnez
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 demanderait 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 :

Image non disponible

Finalement, on commence le tracking.

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

Nous pouvons maintenant mettre en place deux fonctions de rappel qui seront appelées lorsque la position est mise à jour. Cette mise à jour peut fonctionner ou produire une erreur. Chaque cas possède sa fonction de rappel dédiée. Nous allons donc mettre à jour la position sur la carte à chaque fois que c'est possible :

 
Sélectionnez
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 prends 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 très simple à mettre en place.

V. Conclusion

Nous avons pu mettre en place une carte, un marqueur, du geocodage mais aussi de la géolocalisation. Toutes ces fonctionnalités sont très accessibles et vous apporteront de nombreuses nouvelles possibilités en termes d'ergonomie et de fonctionnalités. Il ne faut donc pas vous en priver.

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

VI. Remerciements

Cet article est publié avec l'aimable autorisation de Synbioz, l'article original peut être lu sur le blog de Synbioz : RubyMotion - utilisation de MapKit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Synbioz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.