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é.
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.
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.
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 :
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 :
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.
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 CLLocationCoordinatetitle
: retourne une NSString qui servira comme titresubtitle
: retourne une NSString qui servira comme sous-titreVous 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 :
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 :
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 ?
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 :
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.
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.