diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js
index 41f6b4d1a..f8183b12b 100644
--- a/Examples/UIExplorer/WebViewExample.js
+++ b/Examples/UIExplorer/WebViewExample.js
@@ -96,6 +96,7 @@ var WebViewExample = React.createClass({
url={this.state.url}
javaScriptEnabledAndroid={true}
onNavigationStateChange={this.onNavigationStateChange}
+ onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
startInLoadingState={true}
scalesPageToFit={this.state.scalesPageToFit}
/>
@@ -118,6 +119,11 @@ var WebViewExample = React.createClass({
this.refs[WEBVIEW_REF].reload();
},
+ onShouldStartLoadWithRequest: function(event) {
+ // Implement any custom loading logic here, don't forget to return!
+ return true;
+ },
+
onNavigationStateChange: function(navState) {
this.setState({
backButtonEnabled: navState.canGoBack,
diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js
index 6c7f5484d..9256c3c86 100644
--- a/Libraries/Components/WebView/WebView.ios.js
+++ b/Libraries/Components/WebView/WebView.ios.js
@@ -113,6 +113,12 @@ var WebView = React.createClass({
* user can change the scale
*/
scalesPageToFit: PropTypes.bool,
+
+ /**
+ * Allows custom handling of any webview requests by a JS handler. Return true
+ * or false from this method to continue loading the request.
+ */
+ onShouldStartLoadWithRequest: PropTypes.func,
},
getInitialState: function() {
@@ -158,6 +164,12 @@ var WebView = React.createClass({
webViewStyles.push(styles.hidden);
}
+ var onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => {
+ var shouldStart = this.props.onShouldStartLoadWithRequest &&
+ this.props.onShouldStartLoadWithRequest(event.nativeEvent);
+ RCTWebViewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier);
+ });
+
var webView =
;
diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h
index fdb192a39..3d514dd47 100644
--- a/React/Views/RCTWebView.h
+++ b/React/Views/RCTWebView.h
@@ -9,6 +9,8 @@
#import "RCTView.h"
+@class RCTWebView;
+
/**
* Special scheme used to pass messages to the injectedJavaScript
* code without triggering a page load. Usage:
@@ -17,8 +19,18 @@
*/
extern NSString *const RCTJSNavigationScheme;
+@protocol RCTWebViewDelegate
+
+- (BOOL)webView:(RCTWebView *)webView
+shouldStartLoadForRequest:(NSMutableDictionary *)request
+ withCallback:(RCTDirectEventBlock)callback;
+
+@end
+
@interface RCTWebView : RCTView
+@property (nonatomic, weak) id delegate;
+
@property (nonatomic, strong) NSURL *URL;
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m
index 8b78f1bcf..2c35bc453 100644
--- a/React/Views/RCTWebView.m
+++ b/React/Views/RCTWebView.m
@@ -25,6 +25,7 @@ NSString *const RCTJSNavigationScheme = @"react-js-navigation";
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
+@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
@end
@@ -119,7 +120,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (NSMutableDictionary *)baseEvent
{
- NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{
+ NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{
@"url": _webView.request.URL.absoluteString ?: @"",
@"loading" : @(_webView.loading),
@"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
@@ -142,6 +143,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
+ BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
+
+ // skip this for the JS Navigation handler
+ if (!isJSNavigation && _onShouldStartLoadWithRequest) {
+ NSMutableDictionary *event = [self baseEvent];
+ [event addEntriesFromDictionary: @{
+ @"url": (request.URL).absoluteString,
+ @"navigationType": @(navigationType)
+ }];
+ if (![self.delegate webView:self
+ shouldStartLoadForRequest:event
+ withCallback:_onShouldStartLoadWithRequest]) {
+ return NO;
+ }
+ }
+
if (_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
@@ -156,13 +173,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
}
// JS Navigation handler
- return ![request.URL.scheme isEqualToString:RCTJSNavigationScheme];
+ return !isJSNavigation;
}
- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if (_onLoadingError) {
-
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
// a new URL in the WebView before the previous one came back. We can just
@@ -172,7 +188,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
}
NSMutableDictionary *event = [self baseEvent];
- [event addEntriesFromDictionary: @{
+ [event addEntriesFromDictionary:@{
@"domain": error.domain,
@"code": @(error.code),
@"description": error.localizedDescription,
@@ -185,8 +201,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
{
if (_injectedJavaScript != nil) {
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
+
NSMutableDictionary *event = [self baseEvent];
- [event addEntriesFromDictionary: @{@"jsEvaluationValue":jsEvaluationValue}];
+ event[@"jsEvaluationValue"] = jsEvaluationValue;
+
_onLoadingFinish(event);
}
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m
index 8779a970b..7512000b0 100644
--- a/React/Views/RCTWebViewManager.m
+++ b/React/Views/RCTWebViewManager.m
@@ -14,13 +14,22 @@
#import "RCTUIManager.h"
#import "RCTWebView.h"
-@implementation RCTWebViewManager
+@interface RCTWebViewManager ()
+
+@end
+
+@implementation RCTWebViewManager {
+ NSConditionLock *_shouldStartLoadLock;
+ BOOL _shouldStartLoad;
+}
RCT_EXPORT_MODULE()
- (UIView *)view
{
- return [RCTWebView new];
+ RCTWebView *webView = [RCTWebView new];
+ webView.delegate = self;
+ return webView;
}
RCT_REMAP_VIEW_PROPERTY(url, URL, NSURL);
@@ -34,6 +43,7 @@ RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL);
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock);
+RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock);
- (NSDictionary *)constantsToExport
{
@@ -86,4 +96,38 @@ RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag)
}];
}
+#pragma mark - Exported synchronous methods
+
+- (BOOL)webView:(__unused RCTWebView *)webView
+shouldStartLoadForRequest:(NSMutableDictionary *)request
+ withCallback:(RCTDirectEventBlock)callback
+{
+ _shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()];
+ _shouldStartLoad = YES;
+ request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition);
+ callback(request);
+
+ // Block the main thread for a maximum of 250ms until the JS thread returns
+ if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) {
+ BOOL returnValue = _shouldStartLoad;
+ [_shouldStartLoadLock unlock];
+ _shouldStartLoadLock = nil;
+ return returnValue;
+ } else {
+ RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES");
+ return YES;
+ }
+}
+
+RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier)
+{
+ if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) {
+ _shouldStartLoad = result;
+ [_shouldStartLoadLock unlockWithCondition:0];
+ } else {
+ RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: "
+ "got %zd, expected %zd", lockIdentifier, _shouldStartLoadLock.condition);
+ }
+}
+
@end