Setup
I have a native module that extends RCTEventEmitter and also implements a TurboModule spec:
RCTNativeLocalStorage.h:
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
#import <React/RCTEventEmitter.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTNativeLocalStorage : RCTEventEmitter <NativeLocalStorageSpec>
@end
NS_ASSUME_NONNULL_END
RCTNativeLocalStorage.m (partial):
#import "RCTNativeLocalStorage.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <NativeLocalStorageSpec/NativeLocalStorageSpec.h>
using namespace facebook;
@interface RCTNativeLocalStorage () <CBCentralManagerDelegate, CBPeripheralDelegate>
@property (strong, nonatomic) NSUserDefaults *localStorage;
@property (strong, nonatomic) CBCentralManager *centralManager;
@property (strong, nonatomic) NSMutableArray<NSDictionary *> *discoveredDevices;
@property (nonatomic, assign) BOOL hasListeners;
@end
@implementation RCTNativeLocalStorage
// Register the module
RCT_EXPORT_MODULE(NativeLocalStorage)
// These methods are required for NativeEventEmitter to work properly
RCT_EXPORT_METHOD(addListener:(NSString *)eventName)
{
NSLog(@"π§ addListener called for: %@", eventName);
}
RCT_EXPORT_METHOD(removeListeners:(double)count)
{
NSLog(@"π removeListeners called: %f", count);
}
// Define supported events
- (NSArray<NSString *> *)supportedEvents {
return @[
@"BluetoothDeviceFound",
];
}
// Event listener tracking
- (void)startObserving {
NSLog(@"β
startObserving called - events will be emitted");
self.hasListeners = YES;
}
- (void)stopObserving {
NSLog(@"β οΈ stopObserving called - events will not be emitted");
self.hasListeners = NO;
}
- (instancetype)init {
if (self = [super init]) {
_localStorage = [[NSUserDefaults alloc] initWithSuiteName:@"local-storage"];
_centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
_discoveredDevices = [NSMutableArray new];
_hasListeners = NO;
}
return self;
}
+ (BOOL)requiresMainQueueSetup {
return NO;
}
// TurboModule implementation
- (std::shared_ptr<react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<react::NativeLocalStorageSpecJSI>(params);
}
// MARK: - TurboModule Methods
- (NSString * _Nullable)getItem:(NSString *)key {
return [self.localStorage stringForKey:key];
}
- (void)setItem:(NSString *)value key:(NSString *)key {
[self.localStorage setObject:value forKey:key];
}
- (void)removeItem:(NSString *)key {
[self.localStorage removeObjectForKey:key];
}
- (void)clear {
NSDictionary *allItems = [self.localStorage dictionaryRepresentation];
for (NSString *key in allItems.allKeys) {
[self.localStorage removeObjectForKey:key];
}
}
// Export the startScan method to make it available to JavaScript
RCT_EXPORT_METHOD(startScan) {
NSLog(@"β
startScan triggered from JavaScript");
if (_centralManager.state != CBManagerStatePoweredOn) {
NSLog(@"β Bluetooth not powered on");
return;
}
// Show an alert to verify the method was called
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Scan"
message:@"startScan called!"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *ok = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:ok];
UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;
[root presentViewController:alert animated:YES completion:nil];
});
[_discoveredDevices removeAllObjects];
[_centralManager scanForPeripheralsWithServices:nil options:nil];
}
// Central Manager Delegates
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
switch (central.state) {
case CBManagerStatePoweredOn:
NSLog(@"β
Bluetooth is powered on.");
break;
case CBManagerStatePoweredOff:
NSLog(@"β Bluetooth is powered off.");
break;
default:
NSLog(@"β οΈ Bluetooth state changed: %ld", (long)central.state);
break;
}
}
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
RSSI:(NSNumber *)RSSI {
NSString *deviceName = peripheral.name ?: @"Unknown";
NSString *deviceId = peripheral.identifier.UUIDString;
NSDictionary *deviceInfo = @{
@"name": deviceName,
@"id": deviceId
};
BOOL alreadyExists = NO;
for (NSDictionary *existingDevice in _discoveredDevices) {
if ([existingDevice[@"id"] isEqualToString:deviceId]) {
alreadyExists = YES;
break;
}
}
if (!alreadyExists) {
[_discoveredDevices addObject:deviceInfo];
NSLog(@"β
Device discovered: %@", deviceInfo);
// Send event directly on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// The hasListeners check is important to avoid the warning
if (self.hasListeners) {
NSLog(@"π Sending BluetoothDeviceFound event");
[self sendEventWithName:@"BluetoothDeviceFound" body:deviceInfo];
} else {
NSLog(@"β οΈ No listeners registered for BluetoothDeviceFound event");
}
});
}
}
@end
Javascript Code:
const { NativeLocalStorage } = NativeModules;
const eventEmitter = new NativeEventEmitter(NativeLocalStorage);
useEffect(() => {
console.log('Setting up event listener...');
const subscription = eventEmitter.addListener(
'BluetoothDeviceFound',
(deviceInfo) => {
console.log('Device found:', deviceInfo);
// Update state...
}
);
console.log('Starting scan...');
NativeLocalStorage.startScan();
return () => subscription.remove();
}, []);
Console output after i trigger start scan
β
Bluetooth is powered on.
'`new NativeEventEmitter()` was called with a non-null argument without the required `addListener` method.', { [Component Stack] name: 'Component Stack' }
'`new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method.', { [Component Stack] name: 'Component Stack' }
β
startScan triggered from JavaScript
β
Device discovered: {
id = "E2DEF552-4C7E-FA6F-1CC3-3F6B0DE3CC31";
name = Unknown;
}
β οΈ No listeners registered for BluetoothDeviceFound event
β οΈ No listeners registered for BluetoothDeviceFound event
β οΈ No listeners registered for BluetoothDeviceFound event
The Problem
Unable to send events from iOS native code. The same Code works with android and I am able to send data to the event listeners.
What do you think I am missing here?
Official docs link: iOS Native Modules Β· React Native