Skip to content

Commit 0a290e2

Browse files
javachefacebook-github-bot-4
authored andcommitted
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 facebook/react-native#3643 Reviewed By: nicklockwood Differential Revision: D2600371 fb-gh-sync-id: 14dfdb3df442d899d9f2af831bbc8d695faefa33
1 parent 635edd9 commit 0a290e2

5 files changed

Lines changed: 100 additions & 7 deletions

File tree

Examples/UIExplorer/WebViewExample.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ var WebViewExample = React.createClass({
9696
url={this.state.url}
9797
javaScriptEnabledAndroid={true}
9898
onNavigationStateChange={this.onNavigationStateChange}
99+
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
99100
startInLoadingState={true}
100101
scalesPageToFit={this.state.scalesPageToFit}
101102
/>
@@ -118,6 +119,11 @@ var WebViewExample = React.createClass({
118119
this.refs[WEBVIEW_REF].reload();
119120
},
120121

122+
onShouldStartLoadWithRequest: function(event) {
123+
// Implement any custom loading logic here, don't forget to return!
124+
return true;
125+
},
126+
121127
onNavigationStateChange: function(navState) {
122128
this.setState({
123129
backButtonEnabled: navState.canGoBack,

Libraries/Components/WebView/WebView.ios.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ var WebView = React.createClass({
113113
* user can change the scale
114114
*/
115115
scalesPageToFit: PropTypes.bool,
116+
117+
/**
118+
* Allows custom handling of any webview requests by a JS handler. Return true
119+
* or false from this method to continue loading the request.
120+
*/
121+
onShouldStartLoadWithRequest: PropTypes.func,
116122
},
117123

118124
getInitialState: function() {
@@ -158,6 +164,12 @@ var WebView = React.createClass({
158164
webViewStyles.push(styles.hidden);
159165
}
160166

167+
var onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => {
168+
var shouldStart = this.props.onShouldStartLoadWithRequest &&
169+
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
170+
RCTWebViewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier);
171+
});
172+
161173
var webView =
162174
<RCTWebView
163175
ref={RCT_WEBVIEW_REF}
@@ -173,6 +185,7 @@ var WebView = React.createClass({
173185
onLoadingStart={this.onLoadingStart}
174186
onLoadingFinish={this.onLoadingFinish}
175187
onLoadingError={this.onLoadingError}
188+
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
176189
scalesPageToFit={this.props.scalesPageToFit}
177190
/>;
178191

React/Views/RCTWebView.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
#import "RCTView.h"
1111

12+
@class RCTWebView;
13+
1214
/**
1315
* Special scheme used to pass messages to the injectedJavaScript
1416
* code without triggering a page load. Usage:
@@ -17,8 +19,18 @@
1719
*/
1820
extern NSString *const RCTJSNavigationScheme;
1921

22+
@protocol RCTWebViewDelegate <NSObject>
23+
24+
- (BOOL)webView:(RCTWebView *)webView
25+
shouldStartLoadForRequest:(NSMutableDictionary *)request
26+
withCallback:(RCTDirectEventBlock)callback;
27+
28+
@end
29+
2030
@interface RCTWebView : RCTView
2131

32+
@property (nonatomic, weak) id<RCTWebViewDelegate> delegate;
33+
2234
@property (nonatomic, strong) NSURL *URL;
2335
@property (nonatomic, assign) UIEdgeInsets contentInset;
2436
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;

React/Views/RCTWebView.m

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ @interface RCTWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
2525
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
2626
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
2727
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
28+
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
2829

2930
@end
3031

@@ -119,7 +120,7 @@ - (UIColor *)backgroundColor
119120

120121
- (NSMutableDictionary *)baseEvent
121122
{
122-
NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{
123+
NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{
123124
@"url": _webView.request.URL.absoluteString ?: @"",
124125
@"loading" : @(_webView.loading),
125126
@"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
@@ -142,6 +143,22 @@ - (void)refreshContentInset
142143
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
143144
navigationType:(UIWebViewNavigationType)navigationType
144145
{
146+
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
147+
148+
// skip this for the JS Navigation handler
149+
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
150+
NSMutableDictionary *event = [self baseEvent];
151+
[event addEntriesFromDictionary: @{
152+
@"url": (request.URL).absoluteString,
153+
@"navigationType": @(navigationType)
154+
}];
155+
if (![self.delegate webView:self
156+
shouldStartLoadForRequest:event
157+
withCallback:_onShouldStartLoadWithRequest]) {
158+
return NO;
159+
}
160+
}
161+
145162
if (_onLoadingStart) {
146163
// We have this check to filter out iframe requests and whatnot
147164
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
@@ -156,13 +173,12 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR
156173
}
157174

158175
// JS Navigation handler
159-
return ![request.URL.scheme isEqualToString:RCTJSNavigationScheme];
176+
return !isJSNavigation;
160177
}
161178

162179
- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
163180
{
164181
if (_onLoadingError) {
165-
166182
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
167183
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
168184
// a new URL in the WebView before the previous one came back. We can just
@@ -172,7 +188,7 @@ - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)er
172188
}
173189

174190
NSMutableDictionary *event = [self baseEvent];
175-
[event addEntriesFromDictionary: @{
191+
[event addEntriesFromDictionary:@{
176192
@"domain": error.domain,
177193
@"code": @(error.code),
178194
@"description": error.localizedDescription,
@@ -185,8 +201,10 @@ - (void)webViewDidFinishLoad:(UIWebView *)webView
185201
{
186202
if (_injectedJavaScript != nil) {
187203
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
204+
188205
NSMutableDictionary *event = [self baseEvent];
189-
[event addEntriesFromDictionary: @{@"jsEvaluationValue":jsEvaluationValue}];
206+
event[@"jsEvaluationValue"] = jsEvaluationValue;
207+
190208
_onLoadingFinish(event);
191209
}
192210
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.

React/Views/RCTWebViewManager.m

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@
1414
#import "RCTUIManager.h"
1515
#import "RCTWebView.h"
1616

17-
@implementation RCTWebViewManager
17+
@interface RCTWebViewManager () <RCTWebViewDelegate>
18+
19+
@end
20+
21+
@implementation RCTWebViewManager {
22+
NSConditionLock *_shouldStartLoadLock;
23+
BOOL _shouldStartLoad;
24+
}
1825

1926
RCT_EXPORT_MODULE()
2027

2128
- (UIView *)view
2229
{
23-
return [RCTWebView new];
30+
RCTWebView *webView = [RCTWebView new];
31+
webView.delegate = self;
32+
return webView;
2433
}
2534

2635
RCT_REMAP_VIEW_PROPERTY(url, URL, NSURL);
@@ -34,6 +43,7 @@ - (UIView *)view
3443
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock);
3544
RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock);
3645
RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock);
46+
RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock);
3747

3848
- (NSDictionary *)constantsToExport
3949
{
@@ -86,4 +96,38 @@ - (NSDictionary *)constantsToExport
8696
}];
8797
}
8898

99+
#pragma mark - Exported synchronous methods
100+
101+
- (BOOL)webView:(__unused RCTWebView *)webView
102+
shouldStartLoadForRequest:(NSMutableDictionary *)request
103+
withCallback:(RCTDirectEventBlock)callback
104+
{
105+
_shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()];
106+
_shouldStartLoad = YES;
107+
request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition);
108+
callback(request);
109+
110+
// Block the main thread for a maximum of 250ms until the JS thread returns
111+
if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) {
112+
BOOL returnValue = _shouldStartLoad;
113+
[_shouldStartLoadLock unlock];
114+
_shouldStartLoadLock = nil;
115+
return returnValue;
116+
} else {
117+
RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES");
118+
return YES;
119+
}
120+
}
121+
122+
RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier)
123+
{
124+
if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) {
125+
_shouldStartLoad = result;
126+
[_shouldStartLoadLock unlockWithCondition:0];
127+
} else {
128+
RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: "
129+
"got %zd, expected %zd", lockIdentifier, _shouldStartLoadLock.condition);
130+
}
131+
}
132+
89133
@end

0 commit comments

Comments
 (0)