Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Send event to js from swift or objective-c

I have created the following class (condensed version), heres a reference to the full file https://github.com/cotyembry/CastRemoteNative/blob/7e74dbc56f037cc61241f6ece24a94d8c52abb32/root/ios/CastRemoteNative/NativeMethods.swift

@objc(NativeMethods)
class NativeMethods: RCTEventEmitter {
  @objc(sendEventToJSFromJS)
  func sendEventToJSFromJS {
    self.emitEvent(eventName: "test", body: "bodyTestString")
  }
  func emitEvent(eventName: String: body: Any) {
    self.sendEvent(withName: eventName, body: body)
  }
}

This works perfectly and fires my callback listener that is in my javascript code when I call the emitEvent method like the following, its an altered snippet from https://github.com/cotyembry/CastRemoteNative/blob/7e74dbc56f037cc61241f6ece24a94d8c52abb32/root/js/Components/ChromecastDevicesModal.js

From the javascript side

import {
  NativeModules,
  NativeEventEmitter
} from 'react-native'

//here I bring in the swift class to use inside javascript
var NativeMethods = NativeModules.NativeMethods;

//create an event emitter to use to listen for the native events when they occur
this.eventEmitter = new NativeEventEmitter(NativeMethods);
//listen for the event once it sends
this.subscription = this.eventEmitter.addListener('test', (body) => { console.log('in test event listener callback', body)});

NativeMethods.sendEventToJSFromJS() //call the native method written in swift

I simply have the sendEventToJSFromJS method invoked on a button press in javascript

Again, this works and the console.log('in test event listener callback', body) code works and runs on the javascript side

My Issue where this does NOT work:

If I was to do the following inside the swift file after defining the class, this would not work:

var nativeMethodsInstance = nativeMethods()
nativeMethodsInstance.sendEventToJSFromSwift()

Why? Because the following error is thrown:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'bridge is not set. This is probably because you've explicitly synthesized the bridge in NativeMethods, even though it's inherited from RCTEventEmitter.'

So, when creating an instance of NativeMethods, versus not... what is the difference?

For additional information:

Objective-C gets the same bridge not set issue when I write these same snippets of code in .h and .m files instead of in .swift files

I found where the error message is getting printed in the native code, but it just has the variable

_bridge

and is checking to see if it is nil

The files are this error comes from is:

RCTEventEmitter.h
RCTEventEmitter.c

here is the full snippet of RCTEventEmitter.c

- (void)sendEventWithName:(NSString *)eventName body:(id)body
{


  RCTAssert(_bridge != nil, @"bridge is not set. This is probably because you've "
        "explicitly synthesized the bridge in %@, even though it's inherited "
        "from RCTEventEmitter.", [self class]);

  if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) {
    RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`",
                eventName, [self class], [[self supportedEvents] componentsJoinedByString:@"`, `"]);
  }
  if (_listenerCount > 0) {
    [_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
                    method:@"emit"
                      args:body ? @[eventName, body] : @[eventName]
                completion:NULL];
  } else {
    RCTLogWarn(@"Sending `%@` with no listeners registered.", eventName);
  }
}

Where does this _bridge value get set and how does it get set so I can know, in the cases where it is failing how to set it

I found the following also in RCTEventEmitter.h

@property (nonatomic, weak) RCTBridge *bridge;

In the error that is given it mentions the bridge is inherited in the RCTEventEmitter, so is this maybe an issue with the weak part to the bridge property?

Or do I need to change my strategy in how I'm doing this all together?

I know it probably has to be something to do with me not fully understanding the

@synthesize bridge = _bridge;

part of the code and all the languages being mixed in doesnt help much lol...

This is really hard, so any help would be much appreciated! Thanks so much for your time

here is a link to the full project when the project history code represented the code from my question above (since I have since made changes to the project):

https://github.com/cotyembry/CastRemoteNative/tree/7e74dbc56f037cc61241f6ece24a94d8c52abb32

like image 981
Coty Embry Avatar asked Apr 06 '17 04:04

Coty Embry


1 Answers

I figured it out

Warning: this solution uses a deprecated method react native method - I could not figure out how to "properly" inherit from the RCTEventEmitter and send an event... every time I tried to the _bridge would end up being nil

Make sure Swift is bridged to Objective C (if you're using swift to send the event to javascript)

Do Not create instances of the exported Native modules (whether they be written in Swift or Objective C)

Let React Native's underlying implementation do this and for each and every class that needs to send an event, export that particular Native Class Objective C Implementation code or Swift code (the Native Module) to React-Native. This allows the javascript to be able to listen to the event

var publicBridgeHelperInstance = PublicBridgeHelper()  //instantiate the  the objective c class from inside the .swift file to use later when needing to get a reference to the bridge to send an event to javascript written in react native

@objc(DeviceManager)             //export swift module to objective c
class DeviceManager: NSObject {

  @objc(deviceDidComeOnline:)    //expose the function to objective c
  public func deviceDidComeOnline(_ device: GCKDevice) {
    //imagine this deviceDidComeOnline function gets called from something from the Native code (totally independent of javascript) - honestly this could be called from a native button click as well just to test it works...

    //emit an event to a javascript function that is a in react native Component listening for the event like so:

    //1. get a reference to the bridge to send an event through from Native to Javascript in React Native (here is where my custom code comes in to get this to actually work)
    let rnBridge = publicBridgeHelperInstance.getBridge()  //this gets the bridge that is stored in the AppDelegate.m file that was set from the `rootView.bridge` variable (more on this later)

    //(if you want to print the bridge here to make sure it is not `nil` go ahead:
    print("rnBridge = \(rnBridge)")

    //2. actually send the event through the eventDispatcher
    rnBridge?.eventDispatcher().sendAppEvent(withName: "test", body: "testBody data!!!")
  }
}

in AppDelegate.h put (additionally to the code that was already in the file)

#import "YourProjectsBridgingHeaderToMakeThisCodeAvailableInSwift.h"  //replace this with your actual header you created when creating a swift file (google it if you dont know how to bridge swift to objective c)

@interface PublicBridgeHelper: NSObject
  -(RCTBridge*)getBridge;
@end

in AppDelegate.m put (in addition to the code that was already in the file)

#import <React/RCTRootView.h>

RCTBridge *rnBridgeFromRootView;

@implementation PublicBridgeHelper    //this is created to SIMPLY return rnBridgeFromRootView defined above over to my Swift class when actually sending the event to javascript that defines a react native Component
-(RCTBridge*)getBridge {
  NSLog(@"rnBridgeFromRootView = @%@", rnBridgeFromRootView);
  return rnBridgeFromRootView;
}

important - also make sure to add the following line of code to the Objective C .h's bridging header to make this PublicBridgeHelper definition available to be used in the .swift code

#import "AppDelegate.h"

finally,

now to show you how to set the rnBridgeFromRootView variable used in AppDelegate.m (that gets returned and used in the .swift code right before sending the event to javascript)

open AppDelegate.m and in the method body of

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... }

include the following after the line of code that instantiates the rootView variable

i.e. after the line that probably looks like

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"YourProjecNameProbably" initialProperties:nil launchOptions:launchOptions];

add:

rnBridgeFromRootView = rootView.bridge  //set the bridge to  be exposed and returned later and used by the swift class

Now to explain the publicBridgeHelperInstance.getBridge() part that is in the .swift file

publicBridgeHelper is an instance of an objective c class which allows the swift class ability to get a reference to the react native bridge

If you are still having problems understanding my answer after reading this I made a video over it and you can watch it here:

https://www.youtube.com/watch?v=GZj-Vm9cQIg&t=9s

like image 160
Coty Embry Avatar answered Sep 30 '22 03:09

Coty Embry