Currently, I am in the midst of a project where my goal is to seamlessly integrate React-Native into a native Swift application. To ensure that both sides are kept in sync with the state, I have implemented a 'message bus' - a mechanism designed to facilitate the passing of events between Javascript and native environments.
The process works flawlessly when transmitting an event from JS to iOS; the event is successfully received, parsed, and processed by my Swift code. However, sending an event from Swift to Javascript proves to be more challenging, especially due to inadequate documentation, resulting in the following error:
terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error when sending event: RCTCrossPlatformEventBus.Event with body: { "name" : "Authenticated", "data" : "{\"firstName\":\"1\",\"email\":\"34\",\"lastName\":\"2\",\"password\":\"3\"}" }. RCTCallableJSModules is not set. This is probably because you've explicitly synthesized the RCTCallableJSModules in RCTEventEmitter, even though it's inherited from RCTEventEmitter.'
This particular error seems to be quite common, with numerous questions on Stack Overflow and GitHub issues related to it. Unfortunately, most of the solutions available date back about ±5 years and do not offer much help in resolving my current dilemma. Below, I have outlined my implementation. Any guidance on how to overcome this obstacle would be greatly appreciated.
RCTCrossPlatformEventBus.m
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
@interface RCT_EXTERN_MODULE(RCTCrossPlatformEventBus, RCTEventEmitter)
RCT_EXTERN_METHOD(supportedEvents)
RCT_EXTERN_METHOD(processHybridEvent: (NSString *)name) // this receives JS events
@end
RCTCrossPlatformEventBus.swift
@objc(RCTCrossPlatformEventBus)
open class RCTCrossPlatformEventBus: RCTEventEmitter {
override init() {
super.init()
}
static let appShared = RCTCrossPlatformEventBus()
@objc
public override static func requiresMainQueueSetup() -> Bool {
return true
}
/// Processes a received event received from hybrid code
/// - Parameters:
/// - json: the json encoded string that was sent
@objc func processHybridEvent(_ json: String) {
print("Swift Native processing event: \(json)")
DispatchQueue.main.async {
var jsonObject: [String: Any]?
if let jsonData = json.data(using: .utf8), let dict = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String:Any] {
jsonObject = dict
}
NotificationCenter.default.post(name: .RCTCrossPlatformEventBusEvent, object: self, userInfo: jsonObject)
}
}
/// Posts an event to both the hybrid code
/// - Parameters:
/// - json: the json encoded string that will be sent
@objc func postEvent(json: String) {
self.sendEvent(withName: "RCTCrossPlatformEventBus.Event", body: json)
}
open override func supportedEvents() -> [String]! {
return ["RCTCrossPlatformEventBus.Event"]
}
open override func constantsToExport() -> [AnyHashable : Any]! {
return [:]
}
}
App_Bridging_Header.h
#ifndef ArchitectureDemo_Bridging_Header_h
#define ArchitectureDemo_Bridging_Header_h
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
#import "RCTCrossPlatformEventBus.m"
Then, in Javascript (Typescript actually)
import { NativeModules, NativeEventEmitter } from 'react-native'
import { BehaviorSubject, Observable } from 'rxjs'
const { CrossPlatformEventBus } = NativeModules;
const eventEmitter = new NativeEventEmitter(CrossPlatformEventBus)
class RNCrossPlatformEventBus {
// we set up a private pipeline for events we can post to
private postableEventBus = new BehaviorSubject<string>('')
// and then expose it's observable for everyone to subscribe to
eventBus = this.postableEventBus.asObservable()
constructor() {
eventEmitter.addListener('RCTCrossPlatformEventBus.Event', (body) => {
this.processEventFromNative(body)
})
}
postEvent(json: string) {
this.postableEventBus.next(json)
CrossPlatformEventBus.processHybridEvent(json);
}
processEventFromNative(jsonString: string) {
this.postableEventBus.next(jsonString)
console.log(`React-Native received event from native ${jsonString}`)
}
}
export default new RNCrossPlatformEventBus()