3

I have the following class:

#import "EventHandler.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
#import <pthread.h>

@implementation EventHandler

RCT_EXPORT_MODULE();

@synthesize bridge = _bridge;

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  mach_port_t machTID = pthread_mach_thread_np(pthread_self());
  NSLog(@"Pretending to create an event %@ at %@, current thread: %x", name, location, machTID);
  [self updateLocationEvent];
}

- (void)updateLocationEvent
{
  NSString *eventName = @"name!!!";
  mach_port_t machTID = pthread_mach_thread_np(pthread_self());
  NSLog(@"about to submit event, current thread: %x",machTID);

  [self.bridge.eventDispatcher sendAppEventWithName:@"UpdateLocation"
                                               body:@{@"name": eventName}];
}

@end

My problem: I'm trying with no success to run updateLocationEvent method from main thread method

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations

Which (for those who are not familiar with it) is a method that tracks location whenever there's a significant change. Once inside the updateLocationEvent method while calling from didUpdateLocations the property self.bridge is nil! If you take a look at the other method RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location) I also call updateLocationEvent from there and it emits the event just fine. Actually I just created the method to test if the event was getting fired at all and it did. The class that handles didUpdateLocations has an EventHandler property which is initialized the same way I've seen people do in objective-c:

if (_eventHandler == nil) {
    _eventHandler = [[EventHandler alloc] init];
  }
  [_eventHandler updateLocationEvent];

Perhaps I'm doing this wrong? I'm very new to obj-c and relatively new to react but I just haven't found an example where I can execute an event emitting method from a native method so any help would be greatly appreciated.

3 Answers3

2

The issue is not that you are calling from the main thread, nor is there anything wrong with your module code. The reason it's not working is that the bridge property of the module hasn't been set (it's nil).

The real problem is that you are trying to create your own instance of the module using [[EventHandler alloc] init] so that you can call the updateLocationEvent method, but that isn't how modules are used in React.

You should never init RCTBridgeModules yourself (unless you are passing them into the bridge via the moduleProvider block or extraModules delegate method, but that's probably not relevant here). Modules are instantiated automatically by the bridge when you create it (typically in your app delegate), and they are tied to the bridge's lifecycle. Each bridge instance sets the bridge property of the modules when it instantiates them, so if you create new instances of the module yourself, their bridge property will never be set.

It is possible (though complicated) to access bridge module instances externally by using the [bridge moduleForClass:] method, but the reason we've made it difficult is because it's generally a bad idea. You shouldn't try to call methods on your module externally because you don't know if the bridge has finished loading the JS code yet, or if it's in the process of being torn down after a reload.

The correct way to send events to JS is almost always do it from inside your module code, so that the bridge can manage the lifecycle of your code and make sure it's only called once everything is set up correctly.

Elliot is correct above when he says that RCTLocationObserver already provides this functionality, so I'm not exactly sure why you need to replicate it, but assuming you have your reasons, ideally (as Elliot suggests in his option 2 above), your EventHandler class would be set up as the delegate for CLLocationManager, and would implement the - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations internally. Something like this:

@interface EventHandler () <CLLocationManagerDelegate>
@end

@implementation EventHandler
{
  CLLocationManager *_locationManager;
}

RCT_EXPORT_MODULE();

@synthesize bridge = _bridge;

- (instancetype)init
{
  if ((self = [super init])) {
    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate = self;
    [_locationManager startUpdatingLocation];
  }
  return self;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
  NSString *eventName = @"name!!!";

  [self.bridge.eventDispatcher sendAppEventWithName:@"UpdateLocation"
                                               body:@{@"name": eventName}];
}

@end

This is basically how our own RCTLocationObserver works, although note that in this implementation the user will be prompted to enable location services immediately, whereas you may prefer to export the startUpdatingLocation method to JS so it can be called at the appropriate time in your application.

I'm not sure exactly what Elliot meant with his option no. 3 of using a singleton, as this would suffer from the same problem that the bridge would be unaware of that instance, and so would never set the bridge property for you. I'd also advise against singleton instances of modules in general because they would retain their state after a bridge reload (cmd-R), which may lead to odd bugs or unexpected behaviour. It's better to let the bridge tear down and recreate all the modules every time it is reloaded, which is its default behaviour.

Nick Lockwood
  • 40,865
  • 11
  • 112
  • 103
  • Thanks a lot @Nick Lockwood and @Elliot Lynde! I'll go with sending the event from `didUpdateLocations`. The reason I'm doing it myself is because I'm using `startMonitoringSignificantLocationChanges`. AFAIK it offers waking up the app from suspended or terminated to update whereas `startUpdatingLocation` does not. I tried to use it from an `RCT_EXPORT_METHOD` with `[self performSelectorOnMainThread:@selector(myMethod:) withObject:anObj waitUntilDone:YES];` but it didn't work so I ended up putting the code in AppDelegate (which runs on main thread). – Alejandro Perez Oct 03 '15 at 18:20
  • Wonderful answer. I was stretching my hairs since two days. Finally, this answer helped me a lot to fix my issue. – Nirav Dangi May 04 '16 at 15:47
1

The problem is that when the EventHandler instance is created in this code

if (_eventHandler == nil) {
  _eventHandler = [[EventHandler alloc] init];
}
[_eventHandler updateLocationEvent];

_eventHandler.bridge isn't set. It's usually set automatically during the initialization of the React Native bridge.

A couple of options here:

  1. Check out RCTLocationObserver which may work for your needs.
  2. Make EventHandler be the CLLocationManagerDelegate instead of the other class. See what RCTLocationObserver does, do something similar.
  3. Make EventHandler a singleton and use that instance in the other code.

I'd probably do #1 if possible. If not, #2. If you decide to do #3, you'd add some code something like this in your .m file:

static EventHandler *instance = nil;

+ (EventHandler *)getInstance {    
  if (instance == nil) {
    instance = [[EventHandler alloc] init];
  }
  return instance;    
}

and then use [EventHandler getInstance] to create an instance of EventHandler where you export it as a bridge module. You'd also use it in didUpdateLocations

[[EventHandler getInstance] updateLocationEvent];
0

Note if you're writing a swift module you probably need to do something like

import Foundation

@objc(Foo)
class Foo: NSObject {
  var resolve: RCTPromiseResolveBlock?
  var reject: RCTPromiseRejectBlock?
  
  @objc(requiresMainQueueSetup)
  static func requiresMainQueueSetup() -> Bool {
      return true
  }
  
  // Dispatch methods on main thread
  @objc(methodQueue)
  let methodQueue: DispatchQueue = DispatchQueue.main
...
david_adler
  • 9,690
  • 6
  • 57
  • 97