How to develop an iOS module for iBeacon in React Native?

Contents
In the previous article, we talked about the use of beacons and BLE technology in React Native applications using the react-native-ble library. Now let's move on to a more advanced approach for working with beacons - let's develop a native module for iOS.
Why is it better? You can achieve greater accuracy, deep integration with iOS, and better power management.
In this article, we will compare both approaches and discuss why a native solution is better for certain types of applications. We will also dwell in detail on the technical implementation, setting up beacons and tell you why they are needed for business.

What is the difference between BLE and iBeacon?

Bluetooth Low Energy (BLE) and iBeacon operate on the same technology basis, but are designed for different purposes and have different applications.

BLE (Bluetooth Low Energy)

BLE is designed to transmit small amounts of data over short distances with minimal power consumption. This is ideal for battery-powered devices such as fitness bands, environmental sensors, smartwatches and other Internet of Things (IoT) devices.
The main advantage of BLE is its ability to operate for a long time on a single battery charge due to its low power consumption.
Thanks to BLE, you can create a bunch of different applications that require data transfer between devices over short distances.

iBeacon

iBeacon is a protocol developed by Apple that uses BLE technology to provide geolocation services in specific scenarios. It allows mobile apps to determine their proximity to a beacon, a small BLE device that broadcasts a unique identifier.
Unlike BLE, which is responsible for simple data exchange between devices, iBeacon is used to create more complex interaction scenarios. For example, the application must respond when a user approaches or leaves a specific location.

Benefits of iBeacon

The iBeacon protocol is particularly suitable for applications that require precise and context-sensitive user interactions indoors.
With simplified geofencing setup, better background support, and improved location accuracy. This makes iBeacon ideal for retail marketing, indoor navigation and indoor automation applications.

Simplified geofencing setup

iBeacon makes it easy to create geofences around physical beacons. Developers don't have to worry about the complex setup and maintenance of an indoor GPS network. Because it does not guarantee such accuracy and efficiency. At the same time, iBeacon makes it easy to detect when a user enters or leaves a given geofence.

Better background support

iOS allows the app to run in the background. It will receive a notification when a geofence boundary is crossed even when not in use. Thanks to this, you can, for example, automatically offer coupons or special offers when a person approaches a store. Or track important events for logistics and monitoring applications without active user participation.

Improved indoor location accuracy

In confined spaces such as shopping malls, museums, conference rooms and other indoor locations, GPS is unreliable due to weak signal and lack of accuracy. iBeacon is much better at identifying user locations. This opens the door to the development of more precise indoor navigation systems, allowing, for example, to direct visitors to specific exhibits in a museum or suggest products that are in close proximity to the user in a store.

Beacon

I used the Holyiot nRF52810 beacon. It stands out for its ultra-low power consumption, compact size and waterproof design.
Using the Holyiot-beacon application, you can easily configure the beacon and adapt the parameters to specific needs.

General characteristics

• Signal strength and range:
The beacon is capable of transmitting a signal over a distance of up to 50 meters in open space with adjustable transmission power from -40dB to +4dB. By default, the power is set to +4dB, which optimizes range and power consumption.
• Battery life:
Taking into account the CR2032 battery capacity of 220 mAh and average power consumption of 49uA at +4dB and a transmission interval of 500ms, the beacon provides up to 180 days of operation without battery replacement. This makes the beacon particularly suitable for applications that require long-term deployment without maintenance.
• Waterproof:
The IP67 protection class makes the beacon resistant to water and dust, which expands the possibilities of its use in various conditions.
• Physical parameters:
The beacon has compact dimensions with a diameter of 30 mm and a thickness of 8.4 mm with a weight of only 6.5 g, making it easily integrated into any environment.

Setting up a beacon

1
Download the application
• Open the application store on your smartphone (App Store for iOS or Google Play for Android).
• Search for "Holyiot-beacon" and look for Holyiot's beacon configuration app.
• Install the application on your device
2
Search for device
• Launch the "Holyiot-beacon" application on your smartphone.
• In the application, select the device type to search: beacon, ibeacon or eddystone can be selected depending on your needs.
• The app will start searching for available devices nearby.
3
Connecting to a beacon
• When the beacon is detected, select it from the list of available devices.
• To connect to the beacon and access additional information, click on the "Connect" button.
• When prompted, enter your password to connect. Default password: x`aa14061112`.
4
Beacon configuration
Parameter configuration allows you to fine-tune the beacon's operation to the specific requirements of the application or service, providing the desired balance between visibility, power consumption and positioning accuracy.
After a successful connection, you will be able to view detailed information about the tracker and the parameters available for configuration. In the settings section, select the settings you want to change.
Beacon configuration involves setting various parameters, each of which plays an important role in how the beacon functions and how it interacts with applications and devices. Here are the main parameters that are usually configured when configuring a beacon:
UUID (Universal Unique Identifier)
A UUID is a 128-bit identifier that is used to identify a specific group or category of beacons in a large space. It allows the mobile app to differentiate your organization's beacons.
Major and Minor
Major and Minor are numeric values used to identify a subgroup within a group of beacons with the same UUID. Major usually defines a larger subgroup, and Minor usually defines a smaller one. These options allow you to create additional hierarchy or structuring within your beacon system, making it easier to manage and target interactions within your application.
TX Power (Мощность передачи)
TX Power reflects the signal strength with which the beacon transmits its data. This parameter directly affects the beacon's range and energy consumption. Setting the optimal transmit power allows you to balance the visibility of the beacon and the duration of its battery life.
ADV_interval (Advertising interval)
ADV_interval is the time between successive transmissions of Bluetooth advertising packets from the beacon. The interval affects how often the beacon “announces” itself, which affects power consumption and how quickly applications respond to the beacon appearing or disappearing from range. Decreasing the interval increases the chances of quickly detecting the beacon, but also increases power consumption.
Make the necessary changes and save the settings. Some changes may require reconnecting to the beacon or rebooting the beacon.

Implementing a native module in React Native for iOS

When we were developing an application in React Native, we were looking for a solution for working with iBeacons, but we did not find one that fully met our requirements. Therefore, we decided to develop our own native module for iOS in order to make maximum use of the capabilities of this platform for interacting with beacons.
Creating such a module gave us full control over the process of scanning beacons and processing events associated with their detection, ensuring high reliability and accuracy of the application.

Creating a native module

First, we create a native module for iOS, which we integrate with React Native. To do this, we write code in Swift or Objective-C, which will be called from JavaScript. This process involves declaring methods that can be called from React Native and setting up an event model for exchanging data between the native and JavaScript parts of the application.
For detailed instructions on creating and integrating native modules into a React Native project, including code examples and best practices, see the official React Native documentation.

Setting up required permissions

After creating a native module, the next step is to configure the necessary permissions and capabilities in your Xcode project to ensure it works correctly with beacons and notifications.
Open your project settings in Xcode and go to the "Signing & Capabilities" section.
Here you need to add two key capabilities: "Background Modes" and "Push Notifications".
• Enabling "Location updates" and "Uses Bluetooth LE accessories" in the "Background Modes" section allows the application to receive location data and work with Bluetooth accessories even when it is in the background.
• Enabling "Push Notifications" is necessary so that the application can send local notifications to the user when entering or leaving the beacon's coverage area.
To work correctly with beacons and Bluetooth in iOS, it is important to add the necessary permission lines to the project's Info.plist file. These lines inform the user how the application intends to use the device's data or functionality.
Here are the key permissions that are typically required:
1
Privacy - Location Always and When In Use Usage Description
(NSLocationAlwaysAndWhenInUseUsageDescription): Requires a description of why the application needs access to the user's location data at any time, including in the background.
For example, "This app uses your location to determine your proximity to beacons of interest, even when the app is in the background."
2
Privacy - Location When In Use Usage Description
(`NSBluetoothAlwaysUsageDescription`): Since the module uses Bluetooth to detect beacons, it is necessary to explain why the application needs constant access to Bluetooth.
Example: "Bluetooth access is used to detect beacons around you to provide relevant information and notifications."
3
Privacy - Bluetooth Always Usage Description
(`NSBluetoothAlwaysUsageDescription`): Since the module uses Bluetooth to detect beacons, it is necessary to explain why the application needs constant access to Bluetooth.
Example: "Bluetooth access is used to detect beacons around you to provide relevant information and notifications."
4
Privacy - Bluetooth Peripheral Usage Description
(`NSBluetoothPeripheralUsageDescription`): This explanation is needed if your application is going to act as a Bluetooth peripheral.
For example: "This app uses Bluetooth to communicate with beacons and other devices in your area."

Implementation

BeaconManager.swift

import CoreBluetooth
import CoreLocation
import React
import UserNotifications

// Declare the BeaconManager class, which implements interfaces for working with React Native and delegates for monitoring beacons and Bluetooth'
@objc(BeaconManager)
class BeaconManager: NSObject, RCTBridgeModule, CLLocationManagerDelegate, CBCentralManagerDelegate
{

// Module name for React Native
   static func moduleName() -> String {
     return "BeaconManager"
  }

  private var locationManager: CLLocationManager!
  private var beaconRegion: CLBeaconRegion!
  public var bridge: RCTBridge!
  private var centralManager: CBCentralManager!

// Method for sending local notifications
   func sendLocalNotification(with message: String) {
     let content = UNMutableNotificationContent()
     content.title = message // Notification title
     content.body = "This is a region event" // Notification text
     content.sound = .default // Notification sound

let request = UNNotificationRequest(
       identifier: UUID().uuidString, content: content, trigger: nil) // Create a notification request
     UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) // Adding a request to the notification center
  }

// Start scanning beacons with the given UUID
   @objc func startScanning(_ uuid: String, config: NSDictionary) {
     // Request permission to send notifications
     UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) {
       granted, error in

      if granted {
        print("Notifications allowed")
      } else {
        print("Notifications not allowed")
      }
    }
DispatchQueue.main.async {
       self.locationManager = CLLocationManager() // Initialize CLLocationManager
       self.locationManager.delegate = self // Setting the delegate
       self.locationManager.requestAlwaysAuthorization() // Request for permanent access to geolocation

// Check and set settings for background scanning
       self.locationManager.allowsBackgroundLocationUpdates = true
       self.locationManager.pausesLocationUpdatesAutomatically = false

let uuid = UUID(uuidString: uuid)! // Convert UUID string to UUID
       let beaconConstraint = CLBeaconIdentityConstraint(uuid: uuid) // Create a constraint for the beacon
       self.beaconRegion = CLBeaconRegion(
         beaconIdentityConstraint: beaconConstraint, identifier: "BeaconManagerRegion") // Initialize the beacon region
       self.beaconRegion.notifyOnEntry = true // Notification when entering a region
       self.beaconRegion.notifyOnExit = true // Notification when exiting a region

       self.locationManager.startMonitoring(for: self.beaconRegion) // Start monitoring the region
       self.locationManager.startRangingBeacons(in: self.beaconRegion) // Start determining the distance to beacons in the region
     }
   }

// Stop scanning beacons
   @objc func stopScanning() {
     if let beaconRegion = self.beaconRegion {
       self.locationManager.stopMonitoring(for: beaconRegion) // Stop monitoring the region
       self.locationManager.stopRangingBeacons(in: beaconRegion) // Stop determining the distance to beacons
       self.beaconRegion = nil // Reset the beacon region
       self.locationManager = nil // Reset CLLocationManager
     }
   }

   // Initialize the Bluetooth manager
   @objc func initializeBluetoothManager() {
     centralManager = CBCentralManager(
       delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false])
   }

   // Handle Bluetooth state changes
   func centralManagerDidUpdateState(_ central: CBCentralManager) {
     var msg = ""
     switch central.state {
     case .unknown: msg = "unknown"
     case .resetting: msg = "resetting"
     case .unsupported: msg = "unsupported"
     case .unauthorized: msg = "unauthorized"
     case .poweredOff: msg = "poweredOff"
     case .poweredOn: msg = "poweredOn"
     @unknown default: msg = "unknown"
     }
     bridge.eventDispatcher().sendAppEvent(withName: "onBluetoothStateChanged", body: ["state": msg]) // Send Bluetooth state change event to React Native
   }

// Request for permanent access to geolocation
   @objc func requestAlwaysAuthorization(
     _ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock
   ) {
     let locationManager = CLLocationManager()
     locationManager.delegate = self
     locationManager.requestAlwaysAuthorization()
     let status = CLLocationManager.authorizationStatus()
     let statusString = statusToString(status)
     resolve(["status": statusString])
   }

   // Request for access to geolocation when using the application
   @objc func requestWhenInUseAuthorization(
     _ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock
   ) {
     let locationManager = CLLocationManager()
     locationManager.delegate = self
     locationManager.requestWhenInUseAuthorization()
     let status = CLLocationManager.authorizationStatus()
     let statusString = statusToString(status)
     resolve(["status": statusString])
   }

   // Get the current geolocation permission status
   @objc func getAuthorizationStatus(
     _ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock
   ) {
     let status = CLLocationManager.authorizationStatus()
     resolve(statusToString(status))
   }

// Handling region entry and region exit events
   func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
     if let beaconRegion = region as? CLBeaconRegion {
       sendLocalNotification(with: "Entered region: \(region.identifier)") // Send a notification about entering a region
       if let bridge = self.bridge {
         bridge.eventDispatcher().sendAppEvent(
           withName: "onEnterRegion", body: ["region": beaconRegion.identifier]) // Dispatch a region entry event in React Native
       }
     }
   }

   func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
     if let beaconRegion = region as? CLBeaconRegion {
       sendLocalNotification(with: "Exit region: \(region.identifier)") // Send a notification about leaving the region
       if let bridge = self.bridge {
         bridge.eventDispatcher().sendAppEvent(
           withName: "onExitRegion", body: ["region": beaconRegion.identifier]) // Send a region exit event to React Native
       }
     }
   }

   // Handle detection of beacons in the region
   func locationManager(
     _ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion
   ) {
     let beaconArray = beacons.map { beacon -> [String: Any] in
       return [
         "uuid": beacon.uuid.uuidString, // UUID of the beacon
         "major": beacon.major.intValue, // Major beacon value
         "minor": beacon.minor.intValue, // Minor beacon value
         "distance": beacon.accuracy, // Accuracy of the distance to the beacon
         "rssi": beacon.rssi, // Beacon signal strength
       ]
     }

 if let bridge = bridge {
       bridge.eventDispatcher().sendAppEvent(withName: "onBeaconsDetected", body: beaconArray) // Send data about detected beacons to React Native
     }
   }

   // Handle geolocation permission changes
   func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
     if #available(iOS 14.0, *) {
       if manager.authorizationStatus == .authorizedAlways
         || manager.authorizationStatus == .authorizedWhenInUse
       {
         locationManager.startMonitoring(for: beaconRegion) // Start monitoring the region
         locationManager.startRangingBeacons(in: beaconRegion) // Start determining the distance to beacons
       }
     } else {
       if CLLocationManager.authorizationStatus() == .authorizedAlways
         || CLLocationManager.authorizationStatus() == .authorizedWhenInUse
       {
         locationManager.startMonitoring(for: beaconRegion)
         locationManager.startRangingBeacons(in: beaconRegion)
       }
     }
   }

   //Helper method to convert geolocation permission status to a string
   private func statusToString(_ status: CLAuthorizationStatus) -> String {
     switch status {
     case .notDetermined: return "notDetermined"
     case .restricted: return "restricted"
     case .denied: return "denied"
     case .authorizedAlways: return "authorizedAlways"
     case .authorizedWhenInUse: return "authorizedWhenInUse"
     @unknown default: return "unknown"
     }
   }
}
BeaconManagerBridge.m

#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(BeaconManager, NSObject)

RCT_EXTERN_METHOD(startScanning:(NSString *)uuid config:(NSDictionary *)config)
RCT_EXTERN_METHOD(stopScanning)
RCT_EXTERN_METHOD(requestAlwaysAuthorization:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(requestWhenInUseAuthorization:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getAuthorizationStatus:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(initializeBluetoothManager)

+ (BOOL)requiresMainQueueSetup {
  return YES;
}

@end
App.js

import React, {useEffect, useState} from 'react';
import {View, NativeModules, Text} from 'react-native';
import {DeviceEventEmitter} from 'react-native';

const {BeaconManager} = NativeModules;

BeaconManager.requestAlwaysAuthorization();

BeaconManager.startScanning('FDA50693-A4E2-4FB1-AFCF-C6EB07647825');

const App = () => {
  const [inRegion, setInRegion] = useState(false);
  useEffect(() => {
    DeviceEventEmitter.addListener('onBeaconsDetected', beacons => {
      console.log('onBeaconsDetected', beacons);
    });
  }, []);

  useEffect(() => {
    DeviceEventEmitter.addListener('onEnterRegion', beacons => {
      setInRegion(true);
    });
  }, []);

  useEffect(() => {
    DeviceEventEmitter.addListener('onExitRegion', beacons => {
      setInRegion(false);
    });
  }, []);

  return (
    < View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        paddingHorizontal: 32,
        backgroundColor: inRegion ? '#62BB46' : '#472F92',
      }}>
      <Text style={{color: '#fff', fontWeight: 600, fontSize: 16}}>
        {inRegion
          ? 'You are within the range of the beacon'
          : 'You are out of range of the beacon'}
      </Text>
    </View>
  );
};

export default App;

Working with beacons in the background

The `didEnterRegion` and `didExitRegion` methods are part of the `CLLocationManagerDelegate` protocol and are called when a device enters or leaves the beacon's range, respectively. These techniques allow apps to respond to user location changes even in the background.

Limitations and Features

iOS restrictions on background work

Although iOS allows apps to monitor beacons in the background, there are certain limitations related to battery conservation and device performance.
Apple may limit the frequency of location updates or temporarily pause background app activity to optimize system performance. iOS also provides a limited time to perform the necessary actions in response to this event. This time may vary, but is usually a few seconds. During this time, the application must have time to complete all necessary operations.

Restrictions on sending notifications

While the methods allow you to respond to regions entering and leaving the background, sending notifications to the user is also subject to strict iOS rules. The application must obtain appropriate permission from the user to send notifications.

Response delays

Depending on system settings and environmental conditions, there may be delays between actually entering or leaving a region and when the appropriate methods are called. This is due to both the operating features of Bluetooth and iOS power consumption optimization algorithms.

Beacon accuracy and configuration requirements

To perform optimally in the background, it is important to ensure that the beacons are configured correctly and that the application is configured to work with the required location accuracy.
Working with beacons in the background significantly expands the functionality of applications, allowing you to create advanced solutions for location services, navigation inside buildings, automated user interaction and much more. However, when developing such applications, it is important to consider the features and limitations mentioned above to ensure the best performance and user experience.

If you are interested in this topic and want to figure out how you can use beacons in life, read our article "Interaction with customers offline via mobile app"