Beacons y React Native

Integrar beacons en una aplicación desarrollada con React Native

Image for post
Image for post

Esta semana he estado trasteando con los beacons para un nuevo proyecto en React Native y pese a que de primeras parecen ser bastante fáciles de integrar, me he ido encontrando con casos bastante peculiares que me han hecho plantearme este nuevo artículo.

Para los que no conozcáis esta tecnología, deciros que los beacons son pequeños dispositivos bluetooth de bajo consumo que permiten interactuar con las aplicaciones móviles asociadas cuando nos encontramos próximos a ellos.

Existen multitud de aplicaciones para ellos, aunque es muy común verlos en tiendas con el fin de presentar ofertas y descuentos en determinados productos de forma proactiva. Básicamente el funcionamiento es el siguiente (supongamos una tienda como Zara).

  • El beacon se instala en un área determinada de la tienda, por ejemplo, bufandas.

A poco que penséis seguro que se os ocurren multitud de casos de uso para vuestro negocio/aplicación, por lo que a continuación os detallaré los pasos para integrar los beacon en una aplicación hecha con React Native. ¡Vamos a ello!

Requisitos

Para esta guía necesitaremos:

  • Una aplicación desarrollada en React Native

Y por supuesto… Un beacon! Yo lo adquirí aquí (es una tienda española y comparando precios era de las que mejor salían) y el servicio post-venta ha sido muy bueno (les pregunté una duda con respecto a la configuración y tuve la respuesta prácticamente al momento, por lo que por eso he decidido dejar su enlace):

Pero a poco que rebusquéis en Google hay multitud de opciones así que escoged la que más rabia os de :)

Objetivo

La idea de este tutorial será conseguir que la aplicación reciba notificaciones de proximidad procedentes del beacon estando en background o tras haber sido matada por el usuario..

Configurando React Native Beacon Manager

Lo primero que intenté paras integrar los beacons en mi aplicación fue probar con la librería react-native-beacons-manager la cual podéis encontrar en el siguiente repositorio

la cual, en teoría, nos va a permitir añadir a nuestra aplicación la funcionalidad necesaria para escuchar a los beacons asociados. Sin embargo, posee un problema bastante “gordo” que comentaré posteriormente y que, a mi modo de ver, la hace inservible para los casos habituales en los que queremos emplear beacons en nuestra aplicación.

Image for post
Image for post
Mi cara tras casi un día pegándome con la librería

No obstante, os comentaré el proceso de instalación de la librería ya que es bastante sencillo y, además, nos vendrá bien para la solución que os propongo al problema que tiene.

El primer paso será instalarla y linkarla como habitualmente hacemos, pero no vamos a instalarla del repositorio oficial sino de aquí:

npm install @nois/react-native-beacons-manager// 2react-native link react-native-beacons-manager

¿Por qué? Porque tanto la versión 1.0.7 como la versión 1.1.0 tienen un bug que impide a la aplicación escuchar en background, así que mientras se toman su tiempo para mergear la solución, ése repositorio contiene el código correcto. Básicamente es añadir la línea:

self.locationManager.allowsBackgroundLocationUpdates = true;

al método init de la clase RNiBeacon.m . Así que en vez de perder un par de horas como yo estrunjándoos la cabeza de por qué no funciona, instalad directamente la librería desde ahí. (16/01/2018).

Nota. Voy a centrarme en la configuración para iOS ya que es el dispositivo que tengo. Próximamente actualizaré esta guía añadiendo la configuración para Android, de cara a que tengáis ambas versiones.

A continuación, de cara a asegurarnos que podemos escuchar a nuestro beacon correctamente, iremos a nuestro archivo Info.plist en Xcode y añadiremos la clave:

Privacy - Location When In Usage Description

y la clave (por si las moscas):

Privacy - Bluetooth Peripheral Usage Description

Recordad que es una buena práctica personalizar el mensaje que describe por qué estamos solicitando el acceso de cara a que el usuario esté bien informado de este tipo de situaciones.

Código para gestionar el beacon (iOS) — App en Foreground

Bien, ahora pasamos a la parte interesante. Vamos a añadir el código relacionado con la librería react-native-beacons-manager para interactuar con nuestro beacon.

Para ello crearemos un archivo initBeacon.js en donde añadiremos lo siguiente (dado que solo tenemos un beacon el código de inicialización no tiene mayor complicación):

import Beacons from 'react-native-beacons-manager';const region = {  identifier: 'identifier',  uuid: 'uuid',  major: number,  minor: number};export default function() {  Beacons.requestWhenInUseAuthorization();  Beacons.startMonitoringForRegion(region);  Beacons.startRangingBeaconsInRegion(region);  Beacons.startUpdatingLocation();}

La región también puede ser declarada sin los valores major y minor en el caso de que queráis escuchar todos los beacons que poseen un mismo uuid . El major y minor actúan como categorías para los beacons que cubren el mismo área.

const region = {identifier: 'identifier',uuid: 'uuid'};

La función exportada la ejecutaremos bien en nuestro archivo index.js bien en el método componentWillMount de nuestro componente inicial.

A continuación, nos suscribiremos a las señales emitidas por el beacon y recibidas por nuestro dispositivo gracias a la librería, para lo cual añadiremos lo siguiente a nuestro método componentDidMount :

componentDidMount() {  Beacons.BeaconsEventEmitter.addListener(    ‘beaconsDidRange’,    data => {      const {        region: { identifier },        beacons      } = data;      this.setState({ beacons });

}
);

Beacons.BeaconsEventEmitter.addListener(

'regionDidEnter',

event => {
// code }
);
Beacons.BeaconsEventEmitter.addListener(

'regionDidExit',

event => {
// code });}

¡Ojo! Si habéis instalado la versión 1.10.0 para escuchar las notificaciones será necesario que empleéis Beacons.BeaconsEventEmitter.addListener en vez de DeviceEmitter

Como veis el objeto recibido por la notificación beaconsDidRange contiene el identificador de la región y un array de todos los beacons detectados. Cada uno de los elementos de este array contiene la siguiente información:

  • uuid: uuid del beacon

Por lo que si queremos renderear esta información en una FlatList podemos hacer algo similar a:

renderRow = rowData => {
return (
<View>
<Text>
UUID: {rowData.uuid ? rowData.uuid : ‘NA’}
</Text>
<Text>
Major: {rowData.major ? rowData.major : ‘NA’}
</Text>
<Text>
Minor: {rowData.minor ? rowData.minor : ‘NA’}
</Text>
<Text>
RSSI: {rowData.rssi ? rowData.rssi : ‘NA’}
</Text>
<Text>
Proximity: {rowData.proximity ? rowData.proximity : ‘NA’}
</Text>
<Text>
Distance: {rowData.accuracy ? rowData.accuracy.toFixed(2) : ‘NA’}m
</Text>
</View>
);
}
render() { const { beacons } = this.state; return ( <FlatList data={ beacons } keyExtractor={item => `b--${item.uuid}`} renderItem={({ item } ) => this.renderRow(item)} /> );}

Por otra parte, los eventos regionDidEnter y regionDidExit devuelven tan solo la región ( uuid y identifier ) y son lanzados cuando entramos o salimos de una región (con peculiaridades que comentaré a continuación, así que no os preocupéis si no los estáis recibiendo en este punto del tutorial).

A continuación, si corremos nuestra aplicación, deberíamos obtener una lista de todos los beacons detectados, y entre ellos estará el nuestro (probablemente haya alguno más por la zona si estáis por el centro):

Image for post
Image for post

Código para gestionar el beacon (iOS) — Background

Bien, ahora que ya hemos conseguido conectarnos con nuestro Beacon cuando la aplicación está en primer plano, vamos a intentar configurar la aplicación para funcionar cuando se encuentre en segundo plano. Y es aquí donde van a empezar los problemas.

Image for post
Image for post

⚠️ ¡Importante! Antes de seguir

Antes de seguir os comentaré varias detalles sobre cómo funcionan los beacons en iOS que creo que es importante que conozcáis.

1. Cuando queremos pedirle a iOS que nos avise de la presencia de un beacon que hemos decidido escuchar lo que hacemos es pedirle que nos envíe eventos relacionados con beacons que posean un determinado uuid (y un major y minor si queremos). Aunque cerremos la aplicación o la matemos, iOS nos seguirá avisando de ciertas formas que os diré a continuación.

2. iOS enviará los eventos regionDidEnter y regionDidExit cuando detecte que estamos entrando/saliendo en la región cubierta por un beacon. Da igual el estado en el que se encuentre la aplicación (si en background, foreground o muerta), iOS nos alertará con esos dos eventos pero con algunas peculiaridades:

  • Los eventos no se lanzan inmediatamente, sino que puede que tarden unos 30 segundos en lanzarse para evitar falsos positivos o cruces repetidos en la frontera de la región.
  • El evento didRangeBeacons no despierta a la aplicación, es necesario que se lance cualquiera de los dos eventos anteriores para que este evento empiece a enviarse a nuestra aplicación.

Recursos interesantes para leer:

https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html

Dicho esto, vamos a configurar nuestra aplicación para escuchar en background, ya que esto sirve perfectamente para la solución que propondré al final del artículo.

Habilitar los Background Modes

Para que nuestro la aplicación instalada en nuestro dispositivo pueda escuchar en segundo plano, primero tendremos que habilitar los siguientes background modes de la aplicación desde XCode.

Para ello iremos a Capabilities , seleccionaremos Background Modes y marcaremos: Location Updates y Uses Bluetooth LE accessories :

Image for post
Image for post
From docs of react native background modes

Location Always Usage

Lo siguiente que haremos será añadir al archivo Info.plist las 2 claves (no vale con una sólo):

Privacy — Location Always Usage Description
Privacy - Location Always and When In Use Usage Description

con su correspondiente descripción, ya que ahora necesitaremos que la aplicación escuche constantemente la aplicación.

Además, actualizaremos nuestro archivo initBeacon.js para modificar el método requestWhenInUsageAuthorization por requestAlwaysAuthorization :

import Beacons from 'react-native-beacons-manager';const region = {identifier: 'identifier',uuid: 'uuid',major: number,minor: number};export default function() {Beacons.requestAlwaysAuthorization();Beacons.startMonitoringForRegion(region);Beacons.startRangingBeaconsInRegion(region);Beacons.startUpdatingLocation();}

Hecho esto, podemos lanzar nuestra aplicación, que nos preguntará cuando queremos emplear los servicios de localización y ya podremos dejarla en background para recibir tanto los eventos beaconsDidRange como los eventos regionDidEnter y regionDidExit (podéis probar a quitar la pila de vuestro beacon para forzar a que se lancen estos dos últimos).

Y ahora ya si…. es cuando TODO ESTALLA

Image for post
Image for post

Si matáis vuestra aplicación (es importante para reproducir este bug que la cerréis desde el visor de aplicaciones corriendo) y forzáis un evento regionDidEnter / regionDidExit, estos dos primeros eventos no son recibidos en el listener de javascript (pese a que sí los recibe la clase RNiBeacon).

Esto como comprenderéis es una faena y supone una traba bastante gorda para cualquier aplicación desarrollada en torno a los beacons, ya que no enterarnos de cuando llega el usuario a una región nos imposibilita lanzarle notificaciones o realizar cualquier otra acción.

¿Por qué se pierden? Tras mucho investigar mi opinión apunta a que puesto que es necesario que iOS levante la aplicación en background para responder al evento, al Bridge no le da tiempo a ejecutar el código javascript de nuestra aplicación para adherir los listeners, por lo que la clase RNiBeacon recibe el evento y lo emite al vacío. La sucesión de eventos es algo así:

[BeaconsDemo] AppDelegate: didFinishLaunchingWithOptions enter
[BeaconsDemo] AppDelegate: After jsBundleURLForBundleRoot
[BeaconsDemo] AppDelegate: didFinishLaunchingWithOptions end
// iOS send the event and it is caught by RNiBeacon.m but it has no listeners yet
[BeaconsDemo] regionDidExit
[BeaconsDemo] no listeners in RnIBeacon.m

// First line of javascript --
[BeaconsDemo] start observing
[BeaconsDemo] requestAlwaysAuth

Eso sí, aunque se pierden esos dos eventos comenzamos a recibir el evento beaconsDidRange , es decir, el evento que nos da los beacons localizados en una región, por lo que se nos plantean dos soluciones “fáciles” (la segundo algo más ñapa):

  • Llevar todo el código de gestión de beacons a la parte nativa y olvidarnos de React Native para esta parte

Y también…. podemos reescribir la librería

Image for post
Image for post

Para añadir lo siguiente:

  • La clase RNiBeacon cuando recibe el evento regionDidEnter , regionDidExit guarda el evento en NSUserDefaults indexándolo por el uuid de la región:
NSDate *now = [NSDate date];NSDictionary *regionDict = [self convertBeaconRegionToDict: region];NSDictionary *event = @{  @”region”: regionDict,  @”type”: @”didExit”,  @”date”: [NSNumber        numberWithUnsignedInteger:now.timeIntervalSince1970]};[[NSUserDefaults standardUserDefaults] setObject:event forKey:regionDict[@”uuid”]];[[NSUserDefaults standardUserDefaults] synchronize];

Ahora, cuando vayamos a suscribirnos, la librería nativa nos deberá pasar el último evento recibido (lo he implementado en forma de promesa) de cara a que podamos decidir qué hacer:

RCT_EXPORT_METHOD(startMonitoringForRegion:(NSDictionary *) dictstartMonitoringResolver:(RCTPromiseResolveBlock)resolvestartMonitoringRejecter:(RCTPromiseRejectBlock)reject){  [self.locationManager startMonitoringSignificantLocationChanges];  [self.locationManager startMonitoringForRegion:[self   

convertDictToBeaconRegion:dict]];
NSDictionary *lastEvent = [[NSUserDefaults standardUserDefaults] objectForKey:[dict objectForKey:@”uuid”]]; resolve(lastEvent);}

Como el código javascript se ejecuta una vez recibidos en la parte nativa los eventosregionDidEnter y regionDidExit , lo que haremos será procesar el último evento asociado a la región al suscribirnos a ella:

Beacons.startMonitoringForRegion(region).then(lastEvent => {  if (lastEvent) {    const now = new Date();    if (now.getTime() / 1000 — lastEvent.date < 60) {      if (lastEvent.type === ‘didEnter’) {        regionDidEnter(lastEvent.region);      } else {        regionDidExit(lastEvent.region);      }  }}});

Para que este chiringuito funcione he tenido que reescribir tanto el archivo RNiBeacon.m como el archivo lib/next/new.module.ios.js para añadir tanto la funcionalidad de NSUserDefaults como para trabajar con las nuevas promesas que nos permiten enviar el último evento recibido a la parte de javascript.

Ambos archivos los tenéis aquí por si queréis verlos:

Y con esto ya la aplicación funciona perfectamente. Cuando la matemos y se lance el evento regionDidEnter , este quedará almacenado en NSUserDefaults y, a continuación, cuando se ejecute el método Beacons.startMonitoringForRegion podremos recibirlo y asegurarnos de que ha sucedido lo suficientemente cerca en el tiempo como para que hagamos lo que tengamos que hacer.

Conclusión

Cuando comencé a integrar los beacons en la aplicación me pareció increíblemente fácil realizar la tarea gracias a la librería que os comentaba al principio. Tanto que empecé a sospechar de que algo iría mal. De ahí que haya preparado este post para explicar todas aquellas situaciones con las que os podéis encontrar cuando trabajéis con ella y la forma en que yo las he resuelto.

Probablemente en un tiempo este problema se resuelva pero creo que de momento mi solución es la mejor forma de mantener toda la lógica (o casi toda) en la parte de javascript y evitarnos ñapas como la de comprobar a mano la entrada y salida de una región por medio del evento beaconsDidRange .

En el momento en que me ponga con Android (algo bastante cercano) haré un post similar explicando paso a paso cada una de las cosas que hay que llevar a cabo para integrarlos en este sistema operativo y todas las peculiaridades que me he ido encontrando.

¿Quieres recibir más artículos como este?

Suscríbete a nuestra newsletter:

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store