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 <pj.cabrera@gmail.com>
Closes https://github.com/facebook/react-native/pull/3643

Reviewed By: nicklockwood

Differential Revision: D2600371

fb-gh-sync-id: 14dfdb3df442d899d9f2af831bbc8d695faefa33
This commit is contained in:
Pieter De Baets 2015-11-04 08:42:28 -08:00 committed by facebook-github-bot-4
parent 635edd9ccc
commit 0a290e22da
5 changed files with 100 additions and 7 deletions

View File

@ -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,

View File

@ -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 =
<RCTWebView
ref={RCT_WEBVIEW_REF}
@ -173,6 +185,7 @@ var WebView = React.createClass({
onLoadingStart={this.onLoadingStart}
onLoadingFinish={this.onLoadingFinish}
onLoadingError={this.onLoadingError}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={this.props.scalesPageToFit}
/>;

View File

@ -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 <NSObject>
- (BOOL)webView:(RCTWebView *)webView
shouldStartLoadForRequest:(NSMutableDictionary *)request
withCallback:(RCTDirectEventBlock)callback;
@end
@interface RCTWebView : RCTView
@property (nonatomic, weak) id<RCTWebViewDelegate> delegate;
@property (nonatomic, strong) NSURL *URL;
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;

View File

@ -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.

View File

@ -14,13 +14,22 @@
#import "RCTUIManager.h"
#import "RCTWebView.h"
@implementation RCTWebViewManager
@interface RCTWebViewManager () <RCTWebViewDelegate>
@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