RubyMotion - utilisation de MapKit

Synbioz

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 ♪

- Titre : RubyMotion - utilisation de MapKit

- Auteur : Synbioz

- Parution : 18 mars 2014

- Licence : 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.

Sommaire

- I. Création du projet

- II. Ajout d'une carte par défaut

- II-A. Affichage de la carte

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

- II-C. Ajout d'un marqueur

- II-D. Choix du type de carte

- III. Modification de la localisation

- IV. Détection de la localisation

- V. Conclusion

- VI. Remerciements

I - Création du projet

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

$ 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

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.

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 :

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

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 :

- 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 :

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 causeront 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

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

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 :

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 :

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 :

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 :

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

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 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

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 :

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 :

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 :

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 géocodage, 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.