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:
parent
635edd9ccc
commit
0a290e22da
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue