Published by

Il y a 8 ans -

Temps de lecture 16 minutes

Introduction iBeacon – Programmez

Publié dans le magazine Programmez, numéro 179 novembre 2014, Simone Civetta introduit dans cet article iBeacon.

Depuis quelques années, Apple embarque dans ses iPhones et iPads un nouveau module Bluetooth, de type Low Energy (LE). Ce module, développé pour améliorer la fiabilité et réduire la consommation énergétique, prend aussi en charge un nouveau protocole, le Bluetooth 4.0.

Ces évolutions ont permis à la firme de Cupertino d’introduire un système de géolocalisation indoor baptisé sous le nom marketing iBeacon, depuis iOS 7. Effectivement, nous parlons ici de « nom marketing » car les composants hardware sur lesquels iBeacon repose sont totalement standards et reproductibles par n’importe quel constructeur.

Un cas d’usage, parmi les plus fréquents, est d’utiliser les émetteurs pour localiser un utilisateur à l’intérieur d’une boutique et lui proposer des notifications locales l’informant des produits qui se trouvent à proximité. Depuis quelques mois, Apple – parmi d’autres – implémente donc cette fonctionnalité afin de proposer de l’aide et des promotions personnalisées selon les différents rayons visités.

Mais plus précisément, qu’est-ce qu’un iBeacon ?

Un iBeacon est un petit appareil Bluetooth LE qui émet un signal d’une portée moyenne de 50 mètres. Ce dernier est visible par un terminal implémentant le protocole Bluetooth 4.0 comme les iPhones (à partir du 4S), les iPads (à partir de la 3ème génération) ou les plus récents appareils Android.

Cette borne émettrice (beacon en anglais) est identifiée par la combinaison de trois valeurs : un UUID (souvent l’identifiant univoque du producteur), un numéro majeur et un numéro mineur.

C’est grâce à des APIs haut niveau mises à disposition depuis iOS 7, qu’une application iOS peut facilement détecter les iBeacons émettant à proximité et déclenchant des actions développées préalablement.

Fonctionnement en arrière plan

Concernant iOS 7, les ingénieurs de Cupertino ont décidé d’intégrer largement les iBeacons à l’intérieur du système d’exploitation. Par exemple, les principales interactions entre les émetteurs et l’application peuvent se produire même lorsque celle-ci est inactive voire fermée manuellement par l’utilisateur. Dans ce cas, à proximité d’une borne iBeacon, le système ré-instanciera automatiquement l’application afin de lui permettre d’exécuter des actions implémentées préalablement par le développeur.

Apple est toujours en train d’optimiser l’intégration avec le Bluetooth 4.0 pour réduire la consommation énergétique et accélérer la détection des émetteurs.

Les APIs

Ainsi, un nouveau set d’APIs a été introduit à partir d’iOS 7 pour implémenter les fonctionnalités de géolocalisation indoor. Ces APIs se basent principalement sur les classes CLLocationManager et CLRegion.

CLLocationManager

La classe CLLocationManager s’occupe de la gestion des événements liés à la géolocalisation et à la direction. Il est désormais possible de récupérer les informations de positionnement de l’utilisateur à l’aide des technologies telles que le GPS, la boussole ou, bien sûr, les iBeacons.

La position géographique étant une information sensible pour l’utilisateur, il a la possibilité de refuser l’accès aux données du service de localisation d’une application. C’est à son démarrage que le framework Core Location invitera l’utilisateur à autoriser le service de localisation. Si l’utilisateur refuse, l’objet CLLocationManager signale une erreur appropriée à son delegate. Il est également possible de vérifier l’état de l’autorisation explicite de l’application en utilisant la méthode authorizationStatus.

Afin de configurer et d’utiliser un objet CLLocationManager, il est nécessaire d’affecter un objet personnalisé à la propriété delegate. Cet objet doit être conforme au protocole de CLLocationManagerDelegate.

CLRegion

Une CLRegion est une classe abstraite. Elle identifie une région de l’espace géographique dans laquelle on peut programmer des actions lorsque certains événements ont lieu. Par exemple, nous pouvons déclencher des actions lorsque nous rentrons ou sortons de l’espace identifié par la CLRegion.

CLBeaconRegion

CLBeaconRegion est une sous-classe de CLRegion représentant une borne iBeacon. Comme pour CLRegion, nous pouvons programmer des actions lorsque certains événements se manifestent grâce au location manager.

Il est possible d’identifier une région en utilisant les combinaisons suivantes :

– ProximityUUID ( initWithProximityUUID:identifier: ) : tous les beacons ayant le même UUID feront partie de la même region,

– ProximityUUID et majeur ( initWithProximityUUID:major:identifier: ) : tous les beacons ayant le même UUID et l’attribut « major » constitueront la même region,

– ProximityUUID, majeur et mineur ( initWithProximityUUID:major:minor:identifier: ) : le beacon qui fournit à la fois ces trois valeurs identifiera cette region.

Le paramètre « Identifier », inclus dans les trois constructeurs ci-dessus, représente une chaîne de caractères qui constitue le nom de la CLBeaconRegion. Nous pouvons ainsi repérer facilement la région à l’intérieur de notre code.

Après avoir instancié une région, il est temps de la monitorer, c’est à dire, d’observer les événements de transition. Cette opération est activée grâce à la méthode startMonitoringForRegion: CLLocationManagerDelegate, nous permettant notamment d’être notifié lorsque nous entrons ou sortons d’une région.

Comme expliqué ci-dessus, le monitoring est opéré automatiquement par le système d’exploitation, même quand l’application est en tâche de fond ou totalement terminée. Par conséquent, afin d’éviter que le système ait à prendre en charge un nombre excessif de monitoring, Apple impose une limitation de 20 régions gérées par une application.

Il est donc possible de se désinscrire d’une région et d’annuler le monitoring avec la méthode stopMonitoringForRegion:.

Les changements d’état, les transitions ainsi que les erreurs seront envoyés au delegate de notre CLLocationManager. Celui-ci implémentera le protocole CLLocationManagerDelegate et plus particulièrement des méthodes telles que :

locationManager:didEnterRegion:

locationManager:didExitRegion:

locationManager:didDetermineState:forRegion:

locationManager:monitoringDidFailForRegion:withError:

locationManager:didStartMonitoringForRegion:

CLBeacon

La classe CLBeacon représente un beacon qui a été rencontré lors du monitoring d’une CLRegion. Ce n’est pas à l’utilisateur de créer directement les instances de cette classe.

L’identité d’un beacon est définie par son proximityUUID et par les propriétés majeur et mineur qui font parties des valeurs de configuration d’un beacon.

Observation des beacons dans une région (ranging)

En rentrant dans une région, il est possible de monitorer certaines valeurs propres aux différents beacons qui la composent.

Les méthodes startRangingBeaconsInRegion: et stopRangingBeaconsInRegion: nous permettent d’opérer ce monitoring de géolocalisation.

De plus, il est possible de connaître notre distance par rapport au beacon considéré. Cette information peut prendre une des valeurs suivantes : CLProximityUnknown, CLProximityImmediate, CLProximityNear, CLProximityFar.

Pour calculer avec plus de précision notre distance – en mètres – nous pouvons utiliser la propriété accuracy de l’objet CLBeacon.

Malheureusement, de nombreux facteurs (comme les interférences radio) peuvent réduire la puissance reçue et se répercuter négativement sur la précision de la valeur obtenue.

Matériel

Les iBeacons se basent entièrement sur des implémentations Bluetooth Standard. Cela permet de produire facilement une borne iBeacon à l’aide d’une clé USB Bluetooth LE et d’un Raspberry PI. Il est également possible d’acheter des bornes iBeacons alimentées par une batterie de 3 Volts réalisée par des constructeurs tels que Estimote ou Kontakt.io et vendues à un prix compris entre 20€ et 35€ l’unité. Ces dernières fournissent aussi des interfaces pour la configuration des émetteurs et des SDK spécifiques pour simplifier le développement.

 

 

Let’s code !

Maintenant que nous connaissons la théorie, il est temps de construire une application mobile nous permettant de récupérer les informations des beacons à proximité.

Les bases

Débutons par un nouveau projet de type single view application.

Pour cela, créons une nouvelle classe de type UITableViewController que nous allons nommer XBBeaconViewController. Cette classe contiendra la liste des beacons visibles à proximité.

Dans le storyboard, remplaçons le UIViewController par un UITableViewController. La classe de ce dernier doit prendre la valeur de XBBeaconViewController. À l’intérieur de la tableView, ajoutons deux cellules :

  1.  Une cellule de type Basic, contenant le texte “Aucun beacon visible”, ayant “NoVisibleBeaconCell” en tant que “Identifier”.
  2.  Une cellule vide, de type Subtitle, ayant “BeaconCell” en tant que « Identifier ».

L’interface

Dans le fichier XBBeaconViewController.m, nous y ajoutons une extension et déclarons un objet “locationManagerObserver” de type “id”. Grâce aux NSNotification, nous serons informés de l’état de nos beacons.

 

@interface XBBeaconViewController()@property (nonatomic, strong) id locationManagerObserver;@end

 

Ensuite, nous fournissons l’implémentation des méthodes principales du tableViewController.

 

@implementation XBBeaconViewController- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView reloadData];
[self initObservers];
}- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self.observer];
}- (void)initObservers {
// TODO
}#pragma mark – Table View

– (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *NoVisibleBeaconCellIdentifier = @ »NoVisibleBeaconCell »;
static NSString *BeaconCellIdentifier = @ »BeaconCell »;
// TODO
}

– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// TODO
}

@end

 

Nous sommes maintenant en mesure d’afficher à l’écran une liste (vide, pour l’instant) de beacons.

Le Gestionnaire iBeacon

Continuons en créant une nouvelle classe de type NSObject, nommée XBBeaconLocationManager.

Entête

Le contenu de l’entête XBBeaconLocationManager.h est le suivant :

 

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>extern NSString * const XBBeaconLocationManagerValueChangedNotification;@interface XBBeaconLocationManager : NSObject <CLLocationManagerDelegate>+ (XBBeaconLocationManager *)sharedManager;@property (nonatomic, readonly) NSArray *allBeacons;@end

 

 

XBBeaconLocationManagerValueChangedNotification est le nom de la notification que nous allons envoyer lorsque des nouveaux beacons seront disponibles. Comme mentionné précédemment, pour dialoguer avec le CLLocationManager, notre nouvelle classe est conforme au protocole CLLocationManagerDelegate.

La méthode de classe sharedManager nous permettra d’accéder à une instance singleton de type XBBeaconLocationManager.

Pour terminer avec l’en-tête, la propriété allBeacons nous servira à récupérer la liste des beacons visibles.

Implémentation du géstionnaire iBeacon

 

Nous allons maintenant développer la partie la plus importante de l’application : celle qui s’occupera de récupérer et de renvoyer la liste des bornes iBeacons visibles.

Ouvrons le fichier XBBeaconLocationManager.m et ajoutons les lignes suivantes :

 

NSString * const XBBeaconLocationManagerValueChangedNotification = @ »XBBeaconLocationManagerValueChangedNotification »;@interface XBBeaconLocationManager () <UIAlertViewDelegate>@property (nonatomic, strong) CLLocationManager *locationManager;@property (nonatomic, strong) NSArray *allRegions;@property (nonatomic, strong) NSMutableArray *currentBeacons;

@end

 

Le code que nous avons écrit permet de nommer la constante XBBeaconLocationManagerValueChangedNotification et de déclarer les propriétés qui contiendront le CLLocationManager et la liste des beacons actuels. Le tableau allRegions fournit alors la liste des régions que nous allons monitorer.

Ensuite, ajoutons le code suivant :

 

@implementation XBBeaconLocationManager

 

Puis, créons le constructeur de l’objet singleton :

 

+ (XBBeaconLocationManager *)sharedManager {
static XBBeaconLocationManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[self alloc] init];
});
return sharedManager;
}

 

 

Ensuite, intégrons le constructeur de la classe :

 

– (instancetype)init {
if (self = [super init]) {
[self initAccordingToAuthorizationStatus];
}
return self;
}

 

 

Accédons aux données de géolocalisation

La méthode suivante demande à l’utilisateur la permission d’accéder aux données de géolocalisation, grâce à la fonction de authorisationStatus décrite précédement.

 

– (void)initAccordingToAuthorizationStatus {
if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorized) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@ »Activer iBeacon ? », @ »Activer iBeacon ?”) message:NSLocalizedString(@“Authoriser l’accès à vos donnés de localisation ?”, nil) delegate:self cancelButtonTitle:NSLocalizedString(@ »Non », @“Non ») otherButtonTitles:NSLocalizedString(@ »Oui, continuer », @ »Oui, continuer »), nil];
[alertView show];} else if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusDenied
&& [CLLocationManager authorizationStatus] != kCLAuthorizationStatusRestricted) {
[self initialize];
return;
}
}- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
if (buttonIndex == 1) {
[self initialize];
}
}

 

 

Instancions le CLLocationManager et les régions iBeacon.

 

– (void)initialize {
[self initializeLocationManager];
[self initBeaconRegions];
}

 

À l’aide de la méthode initWithUUIDString décrite dans l’introduction, nous allons créer un nouvel objet de type CLBeaconRegion en spécifiant l’identifiant unique de la région. Dans notre exemple, l’UUID utilisé est celui qui est imposé par défaut dans les bornes du fabriquant Estimote.

Monitorons les régions

Nous pouvons maintenant commencer à monitorer les régions avec startMonitoringForRegion.

La méthode requestStateForRegion: forcera la détection de la région, même si elle avait déjà été repérée.

 

– (void)initBeaconRegions {
CLBeaconRegion *beacon = [[CLBeaconRegion alloc] initWithProximityUUID:
[[NSUUID alloc] initWithUUIDString:@“B9407F30-F5F8-466E-AFF9-25556B57FE6D »]
identifier:@ »Xebia Beacon Region »];// Ajouter plus de beacons si nécessaire
self.allRegions = @[beacon];for (CLBeaconRegion *region in self.allRegions) {
[self.locationManager startMonitoringForRegion:region];
[self.locationManager requestStateForRegion:region];
}
}

 

Enfin, créons le CLLocationManager

 

– (void)initializeLocationManager {
// Initialise un nouveau Location Manager
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
}

 

 

Prenons en charge le protocole CLLocationManagerDelegate

La partie que nous allons maintenant écrire nous est fournie par l’implémentation du protocole CLLocationManagerDelegate.

La méthode didDetermineState est appelée par le CLLocationManager lorsqu’une région est traversée en même temps que locationManager:didEnterRegion: et locationManager:didExitRegion:. Également, le location manager appelle cette méthode en réponse à l’invocation de requestStateForRegion: que nous avons invoqué ci-dessus.

Lorsqu’un changement d’état intervient, nous lançons ou arrêtons la détection précise des iBeacons à l’aide des méthodes startBeaconRanginInRegion: et stopBeaconRangingInRegion:.

 

#pragma mark – CLLocationManagerDelegate- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {if (manager != self.locationManager) {
return;
}if (state == CLRegionStateInside) {
[self startBeaconRangingInRegion:region];
}else if (state == CLRegionStateOutside) {
[self stopBeaconRangingInRegion:region];
}
}

– (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
if (manager != self.locationManager) {
return;
}

[self postValueChangedNotification];
[self startBeaconRangingInRegion:region];
}

– (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
if (manager != self.locationManager) {
return;
}
[self postValueChangedNotification];
[self stopBeaconRangingInRegion:region];
}

 

 

Détectons les beacons

Une fois l’implémentation du monitoring terminée, il est temps de s’occuper du ranging, c’est à dire la détection précise des beacons visibles. Lorsque ces derniers sont repérés, ajoutons les au NSMutableArray currentBeacons et lançons une notification, implémentée ci-dessous.

 

– (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {
self.currentBeacons = [NSMutableArray array];// Trouve le beacon le plus proche
if ([beacons count] > 0) {
CLBeacon *closestBeacon = beacons[0];
if (closestBeacon.proximity == CLProximityImmediate) {
[self.currentBeacons addObject:closestBeacon];
[self postValueChangedNotification];
}// Trouve les autres beacons
else if (closestBeacon.proximity == CLProximityNear) {
[self.currentBeacons addObject:closestBeacon];
[self postValueChangedNotification];
}else {
[self.currentBeacons addObject:closestBeacon];
[self postValueChangedNotification];
}
}
// Aucun beacon n’a été trouvé – le signal a été perdu
else {
[self postValueChangedNotification];
return;
}
}

 

Des implémentations ultérieures du protocole CLLocationManagerDelegate nous permettent de récupérer une des erreurs intervenues dans la détection des beacons.

 

– (void)locationManager:(CLLocationManager *)manager rangingBeaconsDidFailForRegion:(CLBeaconRegion *)region withError:(NSError *)error {
[self postValueChangedNotification];if (error.description) {
NSLog(@ »Location error while ranging. Description: %@ », error.description);
}
}
– (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error {
[self postValueChangedNotification];
if (error.description) {
NSLog(@ »Location error while monitoring. Description: %@ », error.description);
}
}

 

 

Opérons sur l’état de ranging

Les deux méthodes suivantes permettent le lancement et l’arrêt de l’opération de ranging. Par conséquent, une fois évoqué stopRangingBeaconsInRegion, nous ne recevront plus d’appels à locationManager:manager didRangeBeacons:beacons inRegion:region

 

– (void)startBeaconRangingInRegion:(CLRegion *)region {
[self.locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region];
}- (void)stopBeaconRangingInRegion:(CLRegion *)region {
[self.locationManager stopRangingBeaconsInRegion:(CLBeaconRegion *)region];
}

 

Implémentons maintenant les méthodes postValueChangedNotifications et allBeacons. La première envoie à l’aide du NSNotificationCenter une nouvelle notification de nom postNotificationName:XBBeaconLocationManagerValueChangedNotification, tandis que la seconde fournit un accès externe à la liste des beacons calculée précédemment.

 

– (void)postValueChangedNotification {
[[NSNotificationCenter defaultCenter] postNotificationName:XBBeaconLocationManagerValueChangedNotification object:nil userInfo:nil];
}- (NSArray *)allBeacons {
return _currentBeacons;
}

 

Pour conclure avec XBBeaconLocationManager.m, récupérons les changements d’état des autorisations d’accès à la géolocalisation, à l’aide de CLLocationManagerDelegate.

 

– (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
if (status == kCLAuthorizationStatusAuthorized) {
[self initialize];
} else if (status == kCLAuthorizationStatusDenied) {
[[[UIAlertView alloc] initWithTitle:nil
message:NSLocalizedString(@ »Merci d’activer les services de localisation. », nil)
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@ »OK », nil] show];
}
}
@end

 

 

AppDelegate

Afin de créer une instance singleton de XBBeaconLocationManager, nous allons modifier l’AppDelegate de notre application.

 

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self initBeaconTracking];
return YES;
}
– (void)initBeaconTracking {
[XBBeaconLocationManager sharedManager];
}

 

En appellant la méthode sharedManager, l’instance shared (partagée) est chargée en mémoire et commence à détecter les transitions sur les CLBeaconRegion.

Implémentation finale de l’interface

Il ne nous reste plus qu’à afficher les éléments dans le tableau. Revenons au XBBeaconViewController en remplaçant les méthodes suivantes :

Dans initObservers, nous allons écouter les notifications nommées XBBeaconLocationManagerValueChangedNotification envoyées par le singleton XBBeaconLocationManager. Lorsqu’une nouvelle notification est reçue, nous déchargeons les données à l’aide de la méthode reloadData.

 

– (void)initObservers {
__weak typeof(self) weakSelf = self;
self.locationManagerObserver = [[NSNotificationCenter defaultCenter] addObserverForName:XBBeaconLocationManagerValueChangedNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
[weakSelf.tableView reloadData];
}];
}

 

Pour terminer, implémentons cellForRowAtIndexPath et numberOfRowsInSection.

Le premier s’occupe de récupérer les beacons détectés par le XBBeaconLocationManager et de les afficher à l’écran. Si aucun beacon n’est détecté, la cellule vide (“Aucun résultat”) sera affichée à l’écran.

 

– (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *NoVisibleBeaconCellIdentifier = @ »NoVisibleBeaconCell »;
static NSString *BeaconCellIdentifier = @ »BeaconCell »;
NSArray *allBeacons = [XBBeaconLocationManager sharedManager].allBeacons;UITableViewCell *cell;if ([allBeacons count] > 0) {
cell = [tableView dequeueReusableCellWithIdentifier:BeaconCellIdentifier];
CLBeacon *beacon = allBeacons[indexPath.row];
cell.textLabel.text = [[beacon proximityUUID] UUIDString];
cell.detailTextLabel.text = [NSString stringWithFormat:@ »maj: %@, min: %@, dist: %.2fm », [beacon.major stringValue], [beacon.minor stringValue], beacon.accuracy];
} else {
cell = [tableView dequeueReusableCellWithIdentifier:NoVisibleBeaconCellIdentifier];
}return cell;
}

 

Le second, numberOfRowsInSection, nous renvoie le nombre de beacons détectés ou 1 (afin d’afficher la cellule “Aucun résultat”) à l’écran.

 

– (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger allBeacons = [[XBBeaconLocationManager sharedManager].allBeacons count];
return allBeacons ? allBeacons : 1;
}

 

Conclusion

Avec un exemple simple, nous avons montré comment utiliser la technologie iBeacon pour détecter les émetteurs sur nos iDevices. Mais comment faire si nous n’avons pas d’émetteurs ? Il est possible de simuler un iBeacon à l’aide du projet Open Source BeaconOSX, disponible à l’adresse : https://github.com/mttrb/BeaconOSX. Il s’agit d’une application avec une interface graphique simple que l’on peut paramétrer depuis son Mac.

Pour terminer, si vous désirez plus de renseignements ou souhaitez télécharger la totalité du code source de notre application, rendez-vous ici : https://github.com/xebia-france/ibeacon-tutorial-ios.

Published by

Publié par Simone Civetta

Simone est responsable technique des équipes mobilités chez Publicis Sapient Engineering.

Commentaire

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.