diff --git a/Examples/UIExplorer/GeoLocationExample.js b/Examples/UIExplorer/GeoLocationExample.js new file mode 100644 index 000000000..1ab5f290c --- /dev/null +++ b/Examples/UIExplorer/GeoLocationExample.js @@ -0,0 +1,72 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule GeoLocationExample + */ +/* eslint no-console: 0 */ +'use strict'; + + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; + +exports.framework = 'React'; +exports.title = 'GeoLocation'; +exports.description = 'Examples of using the GeoLocation API.'; + +exports.examples = [ + { + title: 'navigator.geolocation', + render: function() { + return ; + }, + } +]; + +var GeoLocationExample = React.createClass({ + getInitialState: function() { + return { + initialPosition: 'unknown', + lastPosition: 'unknown', + }; + }, + + componentDidMount: function() { + navigator.geolocation.getCurrentPosition( + (initialPosition) => this.setState({initialPosition}), + (error) => console.error(error) + ); + this.watchID = navigator.geolocation.watchPosition((lastPosition) => { + this.setState({lastPosition}); + }); + }, + + componentWillUnmount: function() { + navigator.geolocation.clearWatch(this.watchID); + }, + + render: function() { + return ( + + + Initial position: + {JSON.stringify(this.state.initialPosition)} + + + Current position: + {JSON.stringify(this.state.lastPosition)} + + + ); + } +}); + +var styles = StyleSheet.create({ + title: { + fontWeight: 'bold', + }, +}); diff --git a/Examples/UIExplorer/Info.plist b/Examples/UIExplorer/Info.plist index 9a7ca7e3c..245054621 100644 --- a/Examples/UIExplorer/Info.plist +++ b/Examples/UIExplorer/Info.plist @@ -34,6 +34,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSLocationWhenInUseUsageDescription + You need to add NSLocationWhenInUseUsageDescription key in Info.plist to enable geolocation, otherwise it is going to *fail silently*! UIViewControllerBasedStatusBarAppearance diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 56734c70f..f6d55db36 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -31,6 +31,7 @@ var EXAMPLES = [ require('./TouchableExample'), require('./ActivityIndicatorExample'), require('./ScrollViewExample'), + require('./GeoLocationExample'), ]; var UIExplorerList = React.createClass({ diff --git a/Libraries/GeoLocation/GeoLocation.js b/Libraries/GeoLocation/GeoLocation.js new file mode 100644 index 000000000..589931ee9 --- /dev/null +++ b/Libraries/GeoLocation/GeoLocation.js @@ -0,0 +1,98 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule GeoLocation + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTLocationObserver = require('NativeModules').RCTLocationObserver; + +var invariant = require('invariant'); +var logError = require('logError'); +var warning = require('warning'); + +var subscriptions = []; + +var updatesEnabled = false; + +var ensureObserving = function() { + if (!updatesEnabled) { + RCTLocationObserver.startObserving(); + updatesEnabled = true; + } +}; + +/** + * /!\ ATTENTION /!\ + * You need to add NSLocationWhenInUseUsageDescription key + * in Info.plist to enable geolocation, otherwise it's going + * to *fail silently*! + * \!/ \!/ + * + * GeoLocation follows the MDN specification: + * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation + */ +class GeoLocation { + static getCurrentPosition(geo_success, geo_error, geo_options) { + invariant( + typeof geo_success === 'function', + 'Must provide a valid geo_success callback.' + ); + if (geo_options) { + warning('geo_options are not yet supported.'); + } + ensureObserving(); + RCTLocationObserver.getCurrentPosition( + geo_success, + geo_error || logError + ); + } + static watchPosition(callback) { + ensureObserving(); + var watchID = subscriptions.length; + subscriptions.push( + RCTDeviceEventEmitter.addListener( + 'geoLocationDidChange', + callback + ) + ); + return watchID; + } + static clearWatch(watchID) { + var sub = subscriptions[watchID]; + if (!sub) { + // Silently exit when the watchID is invalid or already cleared + // This is consistent with timers + return; + } + sub.remove(); + subscriptions[watchID] = undefined; + var noWatchers = true; + for (var ii = 0; ii < subscriptions.length; ii++) { + if (subscriptions[ii]) { + noWatchers = false; // still valid subscriptions + } + } + if (noWatchers) { + GeoLocation.stopObserving(); + } + } + static stopObserving() { + if (updatesEnabled) { + RCTLocationObserver.stopObserving(); + updatesEnabled = false; + for (var ii = 0; ii < subscriptions.length; ii++) { + if (subscriptions[ii]) { + warning('Called stopObserving with existing subscriptions.'); + subscriptions[ii].remove(); + } + } + subscriptions = []; + } else { + warning('Tried to stop observing when not observing.'); + } + } +} + +module.exports = GeoLocation; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index b529460ee..883a85e03 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -139,9 +139,15 @@ function setupXHR() { GLOBAL.fetch = require('fetch'); } +function setupGeolocation() { + GLOBAL.navigator = GLOBAL.navigator || {}; + GLOBAL.navigator.geolocation = require('GeoLocation'); +} + setupRedBoxErrorHandler(); setupDocumentShim(); setupTimers(); setupAlert(); setupPromise(); setupXHR(); +setupGeolocation(); diff --git a/ReactKit/Modules/RCTLocationObserver.h b/ReactKit/Modules/RCTLocationObserver.h new file mode 100644 index 000000000..ad3ba2ce2 --- /dev/null +++ b/ReactKit/Modules/RCTLocationObserver.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +@interface RCTLocationObserver : NSObject + +@end diff --git a/ReactKit/Modules/RCTLocationObserver.m b/ReactKit/Modules/RCTLocationObserver.m new file mode 100644 index 000000000..315adc842 --- /dev/null +++ b/ReactKit/Modules/RCTLocationObserver.m @@ -0,0 +1,182 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTLocationObserver.h" + +#import +#import + +#import "RCTAssert.h" +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" + +// TODO (#5906496): Shouldn't these be configurable? +const CLLocationAccuracy RCTLocationAccuracy = 500.0; // meters + +@interface RCTPendingLocationRequest : NSObject + +@property (nonatomic, copy) RCTResponseSenderBlock successBlock; +@property (nonatomic, copy) RCTResponseSenderBlock errorBlock; + +@end + +@implementation RCTPendingLocationRequest @end + +@interface RCTLocationObserver () + +@end + +@implementation RCTLocationObserver +{ + CLLocationManager *_locationManager; + RCTEventDispatcher *_eventDispatcher; + NSDictionary *_lastLocationEvent; + NSMutableDictionary *_pendingRequests; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + if (self = [super init]) { + _eventDispatcher = bridge.eventDispatcher; + _pendingRequests = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_locationManager stopUpdatingLocation]; +} + +#pragma mark - Public API + +- (void)startObserving +{ + RCT_EXPORT(); + + dispatch_async(dispatch_get_main_queue(), ^{ + + // Create the location manager if this object does not + // already have one, and it must be created and accessed + // on the main thread + if (nil == _locationManager) { + _locationManager = [[CLLocationManager alloc] init]; + } + + _locationManager.delegate = self; + _locationManager.desiredAccuracy = RCTLocationAccuracy; + + // Set a movement threshold for new events. + _locationManager.distanceFilter = RCTLocationAccuracy; // meters + + if([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { + [_locationManager requestWhenInUseAuthorization]; + } + + [_locationManager startUpdatingLocation]; + + }); +} + +- (void)stopObserving +{ + RCT_EXPORT(); + + dispatch_async(dispatch_get_main_queue(), ^{ + [_locationManager stopUpdatingLocation]; + _lastLocationEvent = nil; + }); +} + +#pragma mark - CLLocationManagerDelegate + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations +{ + CLLocation *loc = [locations lastObject]; + NSDictionary *event = @{ + @"coords": @{ + @"latitude": @(loc.coordinate.latitude), + @"longitude": @(loc.coordinate.longitude), + @"altitude": @(loc.altitude), + @"accuracy": @(RCTLocationAccuracy), + @"altitudeAccuracy": @(RCTLocationAccuracy), + @"heading": @(loc.course), + @"speed": @(loc.speed), + }, + @"timestamp": @(CACurrentMediaTime()) + }; + [_eventDispatcher sendDeviceEventWithName:@"geoLocationDidChange" body:event]; + NSArray *pendingRequestsCopy; + + // TODO (#5906496): is this locking neccessary? If so, use something better than @synchronize + @synchronized(self) { + + pendingRequestsCopy = [_pendingRequests allValues]; + [_pendingRequests removeAllObjects]; + + _lastLocationEvent = event; + } + + for (RCTPendingLocationRequest *request in pendingRequestsCopy) { + if (request.successBlock) { + request.successBlock(@[event]); + } + } +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error +{ + NSArray *pendingRequestsCopy; + + // TODO (#5906496): is this locking neccessary? If so, use something better than @synchronize + @synchronized(self) { + pendingRequestsCopy = [_pendingRequests allValues]; + [_pendingRequests removeAllObjects]; + } + + NSString *errorMsg = @"User denied location service or location service not available."; + for (RCTPendingLocationRequest *request in pendingRequestsCopy) { + if (request.errorBlock) { + request.errorBlock(@[errorMsg]); + } + } +} + +- (void)getCurrentPosition:(RCTResponseSenderBlock)geoSuccess withErrorCallback:(RCTResponseSenderBlock)geoError +{ + RCT_EXPORT(); + + NSDictionary *lastLocationCopy; + // TODO (#5906496): is this locking neccessary? If so, use something better than @synchronize + @synchronized(self) { + if (![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) { + if (geoError) { + NSString *errorMsg = @"User denied location service or location service not available."; + geoError(@[errorMsg]); + return; + } + } + + // If a request for the current position comes in before the OS has informed us, we wait for the first + // OS event and then call our callbacks. This obviates the need for handling of the otherwise + // common failure case of requesting the geolocation until it succeeds, assuming we would have + // instead returned an error if it wasn't yet available. + if (!_lastLocationEvent) { + NSInteger requestID = [_pendingRequests count]; + RCTPendingLocationRequest *request = [[RCTPendingLocationRequest alloc] init]; + request.successBlock = geoSuccess; + request.errorBlock = geoError; + _pendingRequests[@(requestID)] = request; + return; + } else { + lastLocationCopy = [_lastLocationEvent copy]; + } + } + if (geoSuccess) { + geoSuccess(@[lastLocationCopy]); + } +} + +@end diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index 3067cd8e4..721556306 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; }; + 5F5F0D991A9E456B001279FA /* RCTLocationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5F0D981A9E456B001279FA /* RCTLocationObserver.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; @@ -113,6 +114,8 @@ 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+ReactKit.m"; sourceTree = ""; }; 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; 13EFFCCF1A98E6FE002607DC /* RCTJSMethodRegistrar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSMethodRegistrar.h; sourceTree = ""; }; + 5F5F0D971A9E456B001279FA /* RCTLocationObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTLocationObserver.h; sourceTree = ""; }; + 5F5F0D981A9E456B001279FA /* RCTLocationObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTLocationObserver.m; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; @@ -177,6 +180,8 @@ 13B07FE01A69315300A75B9A /* Modules */ = { isa = PBXGroup; children = ( + 5F5F0D971A9E456B001279FA /* RCTLocationObserver.h */, + 5F5F0D981A9E456B001279FA /* RCTLocationObserver.m */, 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */, 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */, 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */, @@ -376,6 +381,7 @@ 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, + 5F5F0D991A9E456B001279FA /* RCTLocationObserver.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 1302F0FD1A78550100EBEF02 /* RCTStaticImage.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */,