From 0a290e22da1e51e4abd7b2a7ee914485024bfff5 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 4 Nov 2015 08:42:28 -0800 Subject: [PATCH] Exporting a synchronous UIWebView to JS Summary: public Original github title: Exported a callback for native webview delegate method shouldStartLoadWithRequest We have a requirement in our app, to open in mobile Safari, any http:// and https:// links displayed in a web view. Our web view is not full screen and is loaded with an HTML string from the backend. Displaying external content in that web view is outside of the scope of our app, so we open them in mobile Safari. I've forked the WebView component and added a callback property, shouldStartLoadWithRequest, and modified the RCTWebView implementation of `webView:shouldStartLoadWithRequest:navigationType:` to check if the shouldStartLoadWithRequest property is set. If the property is set, `webView:shouldStartLoadWithRequest:navigationType:` passes the URL & navigationType to the callback. The callback is then able to ignore the request, redirect it, open a full screen web view to display the URL content, or even deep link to another app with LinkingIOS.openURL(). Original author: PJ Cabrera Closes https://github.com/facebook/react-native/pull/3643 Reviewed By: nicklockwood Differential Revision: D2600371 fb-gh-sync-id: 14dfdb3df442d899d9f2af831bbc8d695faefa33 --- Examples/UIExplorer/WebViewExample.js | 6 +++ Libraries/Components/WebView/WebView.ios.js | 13 ++++++ React/Views/RCTWebView.h | 12 ++++++ React/Views/RCTWebView.m | 28 +++++++++--- React/Views/RCTWebViewManager.m | 48 ++++++++++++++++++++- 5 files changed, 100 insertions(+), 7 deletions(-) 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