Search code examples
javascriptswifttypescriptreact-nativehybrid-mobile-app

React Native: How can I send events from iOS (Swift) back to JavaScript?


I am working on a project where I want to integrate React-Native into a native Swift app. To make sure both sides are aware of state, I've made a 'message bus', A mechanism through which events can be passed from Javascript to native, and vice versa.

This works like a charm when sending an event from JS to iOS; it gets received, parsed and my Swift code knows exactly what to do. Sending an event from Swift to Javascript seems a lot harder - and poorly documented - as I find myself stuck on an 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 error seems to be common as there are a lot of stack overflow questions and GitHub issues to be found on it. However, nearly all of them date back from ±5 years ago, and simply don't help me with my issue any more. Below, I'm listing my implementation. If anybody could provide me with some guidance on how to solve this issue, it would be highly 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()

Solution

  • I resolved my problem in the meantime and am posting the answer here for future reference.

    As it turns out, initializing my RCTCrossPlatformEventBus - as I do in my swift file - Is an invalid operation. React-Native already initialize this, so rather than creating a singleton myself, I just had to override it's initializer like so:

    open class RCTCrossPlatformEventBus: RCTEventEmitter {
        
        public static var shared: RCTCrossPlatformEventBus?
        
        override init() {
            super.init()
            RCTCrossPlatformEventBus.shared = self
        }
        
        ...