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