Using Native Modules for Location Services in React Native iOS
About Location Services
Location-based functionality is a common trope in mobile apps. Typical use cases range anywhere from serving ads relevant to a certain location, to presenting special content at specific places, to out-right navigational aid. If you should find your own react native app would benefit from location services there are a few approaches to take.
The easiest, of course, is pure react native! The documentation leaves a bit to be desired, but if you only need location info while your app is open and do not require any advanced features, you can get location info in your app using an extension to the GeoLocation Web Spec.
In some instances, you may need more customizable location services, or periodic updates in the background. In these instances, there is no avoiding it — we’ve gotta dust off our Objective-C skills and go with a native module. Fortunately, Apple provides the means for us to approach Location Services in a few different ways. In this example, we will make a Native Module that uses Significant-Change Location Service, which provides a good balance between power-saving and accuracy.
Updating info.plist
Let’s first take care of some housekeeping before we start writing code. Open up Xcode or an inline editor and add the following entitlements:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This is needed to get location updates in the background</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This is needed to get location updates when in use</string>
N.B — these keys changed slightly starting with iOS 11, so if you support older versions, the NSLocationAlwaysUsageDescription key is also required.
You’ll also need to open up the Project Editor in Xcode (by double-clicking the top-most node of the Project Navigator). Under the “Targets” section, toggle Background Modes to be ON and check both Location Updates and Background Fetch from the list.
With that done, we are ready to start bridging our javascript to native code!
Adding the Native Module
If you’ve written iOS apps in Swift or Objective C, the process of adding a native module should be very familiar, since a Native Module is just an Objective-C class that implements a native API we can’t get to in React Native via an existing module. We start by creating a header file that will define the module, like so:
#import <React/RCTBridgeModule.h>@interface MyLocationDataManager : NSObject <RCTBridgeModule>
@end
Now, let’s create the module:
#import "MyLocationDataManager.h"
#import
#import
#import
#import @implementation MyLocationDataManager
{
CLLocationManager * locationManager;
NSDictionary * lastLocationEvent;
}- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}RCT_EXPORT_MODULE(MyLocationDataManager);- (NSDictionary *)constantsToExport
{
return @{ @"listOfPermissions": @[@"significantLocationChange"] };
}+ (BOOL)requiresMainQueueSetup
{
return YES; // only do this if your module exports constants or calls UIKit
}//all methods currently async
RCT_EXPORT_METHOD(initialize:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
RCTLogInfo(@"Pretending to do something natively: initialize"); resolve(@(true));
}
RCT_EXPORT_METHOD(hasPermissions:(NSString *)permissionType
hasPermissionsWithResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
RCTLogInfo(@"Pretending to do something natively: hasPermissions %@", permissionType);
BOOL locationAllowed = [CLLocationManager locationServicesEnabled];
resolve(@(locationAllowed));
}RCT_EXPORT_METHOD(requestPermissions:(NSString *)permissionType
requestPermissionsWithResolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *arbitraryReturnVal = @[@"testing..."];
RCTLogInfo(@"Pretending to do something natively: requestPermissions %@", permissionType);
// location
if (!locationManager) {
RCTLogInfo(@"init locationManager...");
locationManager = [[CLLocationManager alloc] init];
}
locationManager.delegate = self;
locationManager.allowsBackgroundLocationUpdates = true;
locationManager.pausesLocationUpdatesAutomatically = true; if ([locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
[locationManager requestAlwaysAuthorization];
} else if ([locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) {
[locationManager requestWhenInUseAuthorization];
} [locationManager startUpdatingLocation];
[locationManager startMonitoringSignificantLocationChanges]; resolve(arbitraryReturnVal);
}- (NSArray *)supportedEvents {
return @[@"significantLocationChange"];
}- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
CLLocation* location = [locations lastObject];
lastLocationEvent = @{
@"coords": @{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
@"altitudeAccuracy": @(location.verticalAccuracy),
@"heading": @(location.course),
@"speed": @(location.speed),
},
@"timestamp": @([location.timestamp timeIntervalSince1970] * 1000) // in ms
}; RCTLogInfo(@"significantLocationChange : %@", lastLocationEvent);
// TODO: do something meaningful with our location event. We can do that here, or emit back to React Native
// https://facebook.github.io/react-native/docs/native-modules-ios.html#sending-events-to-javascript
}@end
It’s important that before we attempt to start getting location info, we request the users permission to use that capability. Notice also, that we request the more intense permission () and fall back to the less demanding option if possible. As is always the case in The Land of Apple, we only have one chance to ask for these permissions in-app (though a user can always modify them from settings), so carefully consider when and where in the app this should occur. Generally speaking, it’s best to ask for permissions only at the time the app starts needing them, so that the user is saturated with requests up front.Once we’ve identified an appropriate place to request location data, we can import our native module and make the request:
// @flow
import { NativeModules } from 'react-native';var location = NativeModules.MyLocationDataManager;async requestPermissions() {
const result = await location.requestPermissions(''); return result;
}
We’re now ready to get location data! From here, we have a few options for using info from our native module. We can subscribe to events, or call custom methods directly as above, depending on our needs. Be sure to review the documentation for the implementation that most suits your needs.For now though, let’s test our native module using basic logging , which we added to MyLocationDataManager.m on line 98. The easiest way to test significant location change is via the simulator, which allows us the control of mocking a location without leaving the comfort of our chairs 🙃Once the simulator is running and the app has started (via normal react-native run-ios, or deployed directly from XCode), then launch the debugging bridge and open an inspector so we can see the console output as it is emitted from the app. Once our debugger is connected, we can navigate in-app to where we request location data. We should see our permission request:
From the Simulator menu, we can update the simulator location of our device from the Debug dropdown:
…and the change is emitted into the console via RCTLogInfo. We did it! With location data verified, we’re now ready to write up whatever more sophisticated implementation(s) suit our needs. But, that’s an exercise for another day 🤔
Troubleshooting Tips
If you’re having trouble getting the location data permissions to show, here are some common questions to ask yourself:
- is info.plist updated with the required entitlements? Are the entitlement keys free of typos?
- is info.plist using the correct keys? (NSLocationAlwaysAndWhenInUseUsageDescription for iOS 11 and beyond and NSLocationAlwaysUsageDescription for anything prior?)
- Are both keys included if older and contemporary iOS versions are supported?
- Have Background Modes been enabled from the Project Editor?
- Have we requested the users’ permission prior to subscribing to location change events or querying for location data explicitly?
- Has permission for location data previously been denied to the app?
Hopefully, this brief look at Native Modules in React Native was illuminating! Stay tuned for a follow-up post about getting location data in Android, and check out the official documentation in the meantime. Shout out to npomfret, whose source repo here heavily influenced the content for my example location manager above.