Authored by hongyong.zhao

use wkwebview instead

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule WebView
* @format
* @noflow
*/
'use strict';
var ActivityIndicator = require('ActivityIndicator');
var EdgeInsetsPropType = require('EdgeInsetsPropType');
var React = require('React');
var PropTypes = require('prop-types');
var ReactNative = require('ReactNative');
var StyleSheet = require('StyleSheet');
var Text = require('Text');
var UIManager = require('UIManager');
var View = require('View');
var ViewPropTypes = require('ViewPropTypes');
var ScrollView = require('ScrollView');
var deprecatedPropType = require('deprecatedPropType');
var invariant = require('fbjs/lib/invariant');
var keyMirror = require('fbjs/lib/keyMirror');
var processDecelerationRate = require('processDecelerationRate');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
var RCTWebViewManager = require('NativeModules').WebViewManager;
var BGWASH = 'rgba(255,255,255,0.8)';
var RCT_WEBVIEW_REF = 'webview';
var WebViewState = keyMirror({
const ActivityIndicator = require('ActivityIndicator');
const EdgeInsetsPropType = require('EdgeInsetsPropType');
const Linking = require('Linking');
const PropTypes = require('prop-types');
const React = require('React');
const ReactNative = require('ReactNative');
const ScrollView = require('ScrollView');
const StyleSheet = require('StyleSheet');
const Text = require('Text');
const UIManager = require('UIManager');
const View = require('View');
const ViewPropTypes = require('ViewPropTypes');
const WebViewShared = require('WebViewShared');
const deprecatedPropType = require('deprecatedPropType');
const invariant = require('fbjs/lib/invariant');
const keyMirror = require('fbjs/lib/keyMirror');
const processDecelerationRate = require('processDecelerationRate');
const requireNativeComponent = require('requireNativeComponent');
const resolveAssetSource = require('resolveAssetSource');
const RCTWKWebViewManager = require('NativeModules').WKWebViewManager;
const BGWASH = 'rgba(255,255,255,0.8)';
const RCT_WEBVIEW_REF = 'webview';
const WebViewState = keyMirror({
IDLE: null,
LOADING: null,
ERROR: null,
... ... @@ -56,7 +57,7 @@ type ErrorEvent = {
domain: any,
code: any,
description: any,
}
};
type Event = Object;
... ... @@ -65,29 +66,24 @@ const DataDetectorTypes = [
'link',
'address',
'calendarEvent',
'trackingNumber',
'flightNumber',
'lookupSuggestion',
'none',
'all',
];
var defaultRenderLoading = () => (
const defaultRenderLoading = () => (
<View style={styles.loadingView}>
<ActivityIndicator />
</View>
);
var defaultRenderError = (errorDomain, errorCode, errorDesc) => (
const defaultRenderError = (errorDomain, errorCode, errorDesc) => (
<View style={styles.errorContainer}>
<Text style={styles.errorTextTitle}>
Error loading page
</Text>
<Text style={styles.errorText}>
{'Domain: ' + errorDomain}
</Text>
<Text style={styles.errorText}>
{'Error Code: ' + errorCode}
</Text>
<Text style={styles.errorText}>
{'Description: ' + errorDesc}
</Text>
<Text style={styles.errorTextTitle}>Error loading page</Text>
<Text style={styles.errorText}>{'Domain: ' + errorDomain}</Text>
<Text style={styles.errorText}>{'Error Code: ' + errorCode}</Text>
<Text style={styles.errorText}>{'Description: ' + errorDesc}</Text>
</View>
);
... ... @@ -116,30 +112,15 @@ var defaultRenderError = (errorDomain, errorCode, errorDesc) => (
class WebView extends React.Component {
static JSNavigationScheme = JSNavigationScheme;
static NavigationType = NavigationType;
static get extraNativeComponentConfig() {
return {
nativeOnly: {
onLoadingStart: true,
onLoadingError: true,
onLoadingFinish: true,
onMessage: true,
messagingEnabled: PropTypes.bool,
},
};
}
static propTypes = {
...ViewPropTypes,
html: deprecatedPropType(
PropTypes.string,
'Use the `source` prop instead.'
'Use the `source` prop instead.',
),
url: deprecatedPropType(
PropTypes.string,
'Use the `source` prop instead.'
),
url: deprecatedPropType(PropTypes.string, 'Use the `source` prop instead.'),
/**
* Loads static html or a uri (with optional headers) in the WebView.
... ... @@ -185,6 +166,12 @@ class WebView extends React.Component {
]),
/**
* If true, use WKWebView instead of UIWebView.
* @platform ios
*/
useWebKit: PropTypes.bool,
/**
* Function that returns a view to show if there's an error.
*/
renderError: PropTypes.func, // view to show if there's an error
... ... @@ -225,7 +212,10 @@ class WebView extends React.Component {
* - fast: 0.99 (the default for iOS web view)
* @platform ios
*/
decelerationRate: ScrollView.propTypes.decelerationRate,
decelerationRate: PropTypes.oneOfType([
PropTypes.oneOf(['fast', 'normal']),
PropTypes.number,
]),
/**
* Boolean value that determines whether scrolling is enabled in the
* `WebView`. The default value is `true`.
... ... @@ -283,6 +273,11 @@ class WebView extends React.Component {
* - `'none'`
* - `'all'`
*
* With the new WebKit implementation, we have three new values:
* - `'trackingNumber'`,
* - `'flightNumber'`,
* - `'lookupSuggestion'`,
*
* @platform ios
*/
dataDetectorTypes: PropTypes.oneOfType([
... ... @@ -328,6 +323,8 @@ class WebView extends React.Component {
* Boolean that controls whether the web content is scaled to fit
* the view and enables the user to change the scale. The default value
* is `true`.
*
* On iOS, when `useWebKit=true`, this prop will not work.
*/
scalesPageToFit: PropTypes.bool,
... ... @@ -357,6 +354,15 @@ class WebView extends React.Component {
mediaPlaybackRequiresUserAction: PropTypes.bool,
/**
* List of origin strings to allow being navigated to. The strings allow
* wildcards and get matched against *just* the origin (not the full URL).
* If the user taps to navigate to a new page but the new page is not in
* this whitelist, we will open the URL in Safari.
* The default whitelisted origins are "http://*" and "https://*".
*/
originWhitelist: PropTypes.arrayOf(PropTypes.string),
/**
* Function that accepts a string that will be passed to the WebView and
* executed immediately as JavaScript.
*/
... ... @@ -372,11 +378,7 @@ class WebView extends React.Component {
* - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content.
* @platform android
*/
mixedContentMode: PropTypes.oneOf([
'never',
'always',
'compatibility'
]),
mixedContentMode: PropTypes.oneOf(['never', 'always', 'compatibility']),
/**
* Override the native component used to render the WebView. Enables a custom native
... ... @@ -393,7 +395,7 @@ class WebView extends React.Component {
*/
props: PropTypes.object,
/*
* Set the ViewManager to use for communcation with the native side.
* Set the ViewManager to use for communication with the native side.
* @platform ios
*/
viewManager: PropTypes.object,
... ... @@ -401,7 +403,7 @@ class WebView extends React.Component {
};
static defaultProps = {
scalesPageToFit: true,
originWhitelist: WebViewShared.defaultOriginWhitelist,
};
state = {
... ... @@ -414,50 +416,90 @@ class WebView extends React.Component {
if (this.props.startInLoadingState) {
this.setState({viewState: WebViewState.LOADING});
}
if (
this.props.useWebKit === true &&
this.props.scalesPageToFit !== undefined
) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
}
render() {
var otherView = null;
let otherView = null;
let scalesPageToFit;
if (this.props.useWebKit) {
({scalesPageToFit} = this.props);
} else {
({scalesPageToFit = true} = this.props);
}
if (this.state.viewState === WebViewState.LOADING) {
otherView = (this.props.renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === WebViewState.ERROR) {
var errorEvent = this.state.lastErrorEvent;
invariant(
errorEvent != null,
'lastErrorEvent expected to be non-null'
);
const errorEvent = this.state.lastErrorEvent;
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
otherView = (this.props.renderError || defaultRenderError)(
errorEvent.domain,
errorEvent.code,
errorEvent.description
errorEvent.description,
);
} else if (this.state.viewState !== WebViewState.IDLE) {
console.error(
'RCTWebView invalid state encountered: ' + this.state.loading
'RCTWebView invalid state encountered: ' + this.state.loading,
);
}
var webViewStyles = [styles.container, styles.webView, this.props.style];
if (this.state.viewState === WebViewState.LOADING ||
this.state.viewState === WebViewState.ERROR) {
const webViewStyles = [styles.container, styles.webView, this.props.style];
if (
this.state.viewState === WebViewState.LOADING ||
this.state.viewState === WebViewState.ERROR
) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
const nativeConfig = this.props.nativeConfig || {};
const viewManager = nativeConfig.viewManager || RCTWebViewManager;
let viewManager = nativeConfig.viewManager;
var onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => {
var shouldStart = this.props.onShouldStartLoadWithRequest &&
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
viewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier);
});
viewManager = viewManager || RCTWKWebViewManager;
const compiledWhitelist = [
'about:blank',
...(this.props.originWhitelist || []),
].map(WebViewShared.originWhitelistToRegex);
const onShouldStartLoadWithRequest = (event: Event) => {
let shouldStart = true;
const {url} = event.nativeEvent;
const origin = WebViewShared.extractOrigin(url);
const passesWhitelist = compiledWhitelist.some(x =>
new RegExp(x).test(origin),
);
shouldStart = shouldStart && passesWhitelist;
if (!passesWhitelist) {
Linking.openURL(url);
}
if (this.props.onShouldStartLoadWithRequest) {
shouldStart =
shouldStart &&
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
}
viewManager.startLoadWithResult(
!!shouldStart,
event.nativeEvent.lockIdentifier,
);
};
var decelerationRate = processDecelerationRate(this.props.decelerationRate);
const decelerationRate = processDecelerationRate(
this.props.decelerationRate,
);
var source = this.props.source || {};
const source = this.props.source || {};
if (this.props.html) {
source.html = this.props.html;
} else if (this.props.url) {
... ... @@ -466,9 +508,15 @@ class WebView extends React.Component {
const messagingEnabled = typeof this.props.onMessage === 'function';
const NativeWebView = nativeConfig.component || RCTWebView;
let NativeWebView = nativeConfig.component;
var webView =
if (this.props.useWebKit) {
NativeWebView = NativeWebView || RCTWKWebView;
} else {
NativeWebView = NativeWebView || RCTWebView;
}
const webView = (
<NativeWebView
ref={RCT_WEBVIEW_REF}
key="webViewKey"
... ... @@ -479,19 +527,24 @@ class WebView extends React.Component {
scrollEnabled={this.props.scrollEnabled}
decelerationRate={decelerationRate}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
automaticallyAdjustContentInsets={
this.props.automaticallyAdjustContentInsets
}
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={this.props.scalesPageToFit}
scalesPageToFit={scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
mediaPlaybackRequiresUserAction={
this.props.mediaPlaybackRequiresUserAction
}
dataDetectorTypes={this.props.dataDetectorTypes}
{...nativeConfig.props}
/>;
/>
);
return (
<View style={styles.container}>
... ... @@ -501,14 +554,22 @@ class WebView extends React.Component {
);
}
_getCommands() {
if (!this.props.useWebKit) {
return UIManager.RCTWebView.Commands;
}
return UIManager.RCTWKWebView.Commands;
}
/**
* Go forward one page in the web view's history.
*/
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goForward,
null
this._getCommands().goForward,
null,
);
};
... ... @@ -518,8 +579,8 @@ class WebView extends React.Component {
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goBack,
null
this._getCommands().goBack,
null,
);
};
... ... @@ -530,8 +591,8 @@ class WebView extends React.Component {
this.setState({viewState: WebViewState.LOADING});
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.reload,
null
this._getCommands().reload,
null,
);
};
... ... @@ -541,8 +602,8 @@ class WebView extends React.Component {
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.stopLoading,
null
this._getCommands().stopLoading,
null,
);
};
... ... @@ -556,25 +617,25 @@ class WebView extends React.Component {
* document.addEventListener('message', e => { document.title = e.data; });
* ```
*/
postMessage = (data) => {
postMessage = data => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.postMessage,
[String(data)]
this._getCommands().postMessage,
[String(data)],
);
};
/**
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = (data) => {
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = data => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.injectJavaScript,
[data]
this._getCommands().injectJavaScript,
[data],
);
};
... ... @@ -596,26 +657,26 @@ class WebView extends React.Component {
};
_onLoadingStart = (event: Event) => {
var onLoadStart = this.props.onLoadStart;
const onLoadStart = this.props.onLoadStart;
onLoadStart && onLoadStart(event);
this._updateNavigationState(event);
};
_onLoadingError = (event: Event) => {
event.persist(); // persist this event because we need to store it
var {onError, onLoadEnd} = this.props;
const {onError, onLoadEnd} = this.props;
onError && onError(event);
onLoadEnd && onLoadEnd(event);
console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: WebViewState.ERROR
viewState: WebViewState.ERROR,
});
};
_onLoadingFinish = (event: Event) => {
var {onLoad, onLoadEnd} = this.props;
const {onLoad, onLoadEnd} = this.props;
onLoad && onLoad(event);
onLoadEnd && onLoadEnd(event);
this.setState({
... ... @@ -625,14 +686,47 @@ class WebView extends React.Component {
};
_onMessage = (event: Event) => {
var {onMessage} = this.props;
const {onMessage} = this.props;
onMessage && onMessage(event);
};
componentDidUpdate(prevProps) {
if (!(prevProps.useWebKit && this.props.useWebKit)) {
return;
}
this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
if (this.props.scalesPageToFit !== undefined) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
}
_showRedboxOnPropChanges(prevProps, propName: string) {
if (this.props[propName] !== prevProps[propName]) {
console.error(
`Changes to property ${propName} do nothing after the initial render.`,
);
}
}
}
var RCTWebView = requireNativeComponent('RCTWebView', WebView, WebView.extraNativeComponentConfig);
const RCTWebView = requireNativeComponent(
'RCTWebView',
WebView,
WebView.extraNativeComponentConfig,
);
const RCTWKWebView = requireNativeComponent(
'RCTWKWebView',
WebView,
WebView.extraNativeComponentConfig,
);
var styles = StyleSheet.create({
const styles = StyleSheet.create({
container: {
flex: 1,
},
... ... @@ -665,7 +759,7 @@ var styles = StyleSheet.create({
},
webView: {
backgroundColor: '#ffffff',
}
},
});
module.exports = WebView;
... ...
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTView.h>
#import <React/RCTDefines.h>
#import <WebKit/WebKit.h>
@class RCTWebView;
@class RCTWKWebView;
/**
* Special scheme used to pass messages to the injectedJavaScript
* code without triggering a page load. Usage:
*
* window.location.href = RCTJSNavigationScheme + '://hello'
*/
extern NSString *const RCTJSNavigationScheme;
@protocol RCTWebViewDelegate <NSObject>
@protocol RCTWKWebViewDelegate <NSObject>
- (BOOL)webView:(RCTWebView *)webView
- (BOOL)webView:(RCTWKWebView *)webView
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
withCallback:(RCTDirectEventBlock)callback;
@end
@interface RCTWebView : RCTView
@property (nonatomic, weak) id<RCTWebViewDelegate> delegate;
@interface RCTWKWebView : RCTView
@property (nonatomic, weak) id<RCTWKWebViewDelegate> delegate;
@property (nonatomic, copy) NSDictionary *source;
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, assign) BOOL messagingEnabled;
@property (nonatomic, copy) NSString *injectedJavaScript;
@property (nonatomic, assign) BOOL scalesPageToFit;
@property (nonatomic, assign) BOOL scrollEnabled;
@property (nonatomic, assign) CGFloat decelerationRate;
@property (nonatomic, assign) BOOL allowsInlineMediaPlayback;
@property (nonatomic, assign) BOOL bounces;
@property (nonatomic, assign) BOOL mediaPlaybackRequiresUserAction;
#if WEBKIT_IOS_10_APIS_AVAILABLE
@property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes;
#endif
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
- (void)postMessage:(NSString *)message;
- (void)injectJavaScript:(NSString *)script;
- (void)goForward;
- (void)goBack;
- (void)reload;
- (void)stopLoading;
- (void)postMessage:(NSString *)message;
- (void)injectJavaScript:(NSString *)script;
@end
... ...
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTWKWebView.h"
#import <React/RCTConvert.h>
#import "RCTAutoInsetsProtocol.h"
static NSString *const MessageHanderName = @"ReactNative";
@interface RCTWKWebView () <WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIScrollViewDelegate, RCTAutoInsetsProtocol>
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
@property (nonatomic, copy) WKWebView *webView;
@end
@implementation RCTWKWebView
{
UIColor * _savedBackgroundColor;
}
- (void)dealloc
{
}
/**
* See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html.
*/
+ (BOOL)dynamicallyLoadWebKitIfAvailable
{
static BOOL _webkitAvailable=NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSBundle *webKitBundle = [NSBundle bundleWithPath:@"/System/Library/Frameworks/WebKit.framework"];
if (webKitBundle) {
_webkitAvailable = [webKitBundle load];
}
});
return _webkitAvailable;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
super.backgroundColor = [UIColor clearColor];
_bounces = YES;
_scrollEnabled = YES;
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
}
return self;
}
- (void)didMoveToWindow
{
if (self.window != nil) {
if (![[self class] dynamicallyLoadWebKitIfAvailable]) {
return;
};
WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
wkWebViewConfig.userContentController = [WKUserContentController new];
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
#if WEBKIT_IOS_10_APIS_AVAILABLE
wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
? WKAudiovisualMediaTypeAll
: WKAudiovisualMediaTypeNone;
wkWebViewConfig.dataDetectorTypes = _dataDetectorTypes;
#else
wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction;
#endif
_webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig];
_webView.scrollView.delegate = self;
_webView.UIDelegate = self;
_webView.navigationDelegate = self;
_webView.scrollView.scrollEnabled = _scrollEnabled;
_webView.scrollView.bounces = _bounces;
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
_webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
#endif
[self addSubview:_webView];
[self visitSource];
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
_savedBackgroundColor = backgroundColor;
if (_webView == nil) {
return;
}
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
self.opaque = _webView.opaque = (alpha == 1.0);
_webView.scrollView.backgroundColor = backgroundColor;
_webView.backgroundColor = backgroundColor;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window.webkit.messageHandlers.[MessageHanderName].postMessage
*/
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message
{
if (_onMessage != nil) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{@"data": message.body}];
_onMessage(event);
}
}
- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
_source = [source copy];
if (_webView != nil) {
[self visitSource];
}
}
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
_contentInset = contentInset;
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:NO];
}
- (void)refreshContentInset
{
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:YES];
}
- (void)visitSource
{
// Check for a static html source first
NSString *html = [RCTConvert NSString:_source[@"html"]];
if (html) {
NSURL *baseURL = [RCTConvert NSURL:_source[@"baseUrl"]];
if (!baseURL) {
baseURL = [NSURL URLWithString:@"about:blank"];
}
[_webView loadHTMLString:html baseURL:baseURL];
return;
}
NSURLRequest *request = [RCTConvert NSURLRequest:_source];
// Because of the way React works, as pages redirect, we actually end up
// passing the redirect urls back here, so we ignore them if trying to load
// the same url. We'll expose a call to 'reload' to allow a user to load
// the existing page.
if ([request.URL isEqual:_webView.URL]) {
return;
}
if (!request.URL) {
// Clear the webview
[_webView loadHTMLString:@"" baseURL:nil];
return;
}
[_webView loadRequest:request];
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
scrollView.decelerationRate = _decelerationRate;
}
- (void)setScrollEnabled:(BOOL)scrollEnabled
{
_scrollEnabled = scrollEnabled;
_webView.scrollView.scrollEnabled = scrollEnabled;
}
- (void)postMessage:(NSString *)message
{
NSDictionary *eventInitDict = @{@"data": message};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[self evaluateJS: source thenCall: nil];
}
- (void)layoutSubviews
{
[super layoutSubviews];
// Ensure webview takes the position and dimensions of RCTWKWebView
_webView.frame = self.bounds;
}
- (NSMutableDictionary<NSString *, id> *)baseEvent
{
NSDictionary *event = @{
@"url": _webView.URL.absoluteString ?: @"",
@"title": _webView.title,
@"loading" : @(_webView.loading),
@"canGoBack": @(_webView.canGoBack),
@"canGoForward" : @(_webView.canGoForward)
};
return [[NSMutableDictionary alloc] initWithDictionary: event];
}
#pragma mark - WKNavigationDelegate methods
/**
* Decides whether to allow or cancel a navigation.
* @see https://fburl.com/42r9fxob
*/
- (void) webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
navigationTypes = @{
@(WKNavigationTypeLinkActivated): @"click",
@(WKNavigationTypeFormSubmitted): @"formsubmit",
@(WKNavigationTypeBackForward): @"backforward",
@(WKNavigationTypeReload): @"reload",
@(WKNavigationTypeFormResubmitted): @"formresubmit",
@(WKNavigationTypeOther): @"other",
};
});
WKNavigationType navigationType = navigationAction.navigationType;
NSURLRequest *request = navigationAction.request;
if (_onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
if (![self.delegate webView:self
shouldStartLoadForRequest:event
withCallback:_onShouldStartLoadWithRequest]) {
decisionHandler(WKNavigationResponsePolicyCancel);
return;
}
}
if (_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
if (isTopFrame) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
_onLoadingStart(event);
}
}
// Allow all navigation by default
decisionHandler(WKNavigationResponsePolicyAllow);
}
/**
* Called when an error occurs while the web view is loading content.
* @see https://fburl.com/km6vqenw
*/
- (void) webView:(WKWebView *)webView
didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(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
// ignore these since they aren't real errors.
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
return;
}
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"didFailProvisionalNavigation": @YES,
@"domain": error.domain,
@"code": @(error.code),
@"description": error.localizedDescription,
}];
_onLoadingError(event);
}
[self setBackgroundColor: _savedBackgroundColor];
}
- (void)evaluateJS:(NSString *)js
thenCall: (void (^)(NSString*)) callback
{
[self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
if (error == nil && callback != nil) {
callback([NSString stringWithFormat:@"%@", result]);
}
}];
}
/**
* Called when the navigation is complete.
* @see https://fburl.com/rtys6jlb
*/
- (void) webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation
{
if (_messagingEnabled) {
#if RCT_DEV
// Implementation inspired by Lodash.isNative.
NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))";
[self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) {
if (! [result isEqualToString:@"true"]) {
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
}];
#endif
NSString *source = [NSString stringWithFormat:
@"(function() {"
"window.originalPostMessage = window.postMessage;"
"window.postMessage = function(data) {"
"window.webkit.messageHandlers.%@.postMessage(String(data));"
"};"
"})();",
MessageHanderName
];
[self evaluateJS: source thenCall: nil];
}
if (_injectedJavaScript) {
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
NSMutableDictionary *event = [self baseEvent];
event[@"jsEvaluationValue"] = jsEvaluationValue;
if (self.onLoadingFinish) {
self.onLoadingFinish(event);
}
}];
} else if (_onLoadingFinish) {
_onLoadingFinish([self baseEvent]);
}
[self setBackgroundColor: _savedBackgroundColor];
}
- (void)injectJavaScript:(NSString *)script
{
[self evaluateJS: script thenCall: nil];
}
- (void)goForward
{
[_webView goForward];
}
- (void)goBack
{
[_webView goBack];
}
- (void)reload
{
/**
* When the initial load fails due to network connectivity issues,
* [_webView reload] doesn't reload the webpage. Therefore, we must
* manually call [_webView loadRequest:request].
*/
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
if (request.URL && !_webView.URL.absoluteString.length) {
[_webView loadRequest:request];
}
else {
[_webView reload];
}
}
- (void)stopLoading
{
[_webView stopLoading];
}
- (void)setBounces:(BOOL)bounces
{
_bounces = bounces;
_webView.scrollView.bounces = bounces;
}
@end
... ...
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
@interface RCTWebViewManager : RCTViewManager
@interface RCTWKWebViewManager : RCTViewManager
@end
... ...
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTWebViewManager.h"
#import "RCTWKWebViewManager.h"
#import "RCTBridge.h"
#import "RCTUIManager.h"
#import "RCTWebView.h"
#import "UIView+React.h"
@interface RCTWebViewManager () <RCTWebViewDelegate>
#import "RCTWKWebView.h"
#import <React/RCTDefines.h>
@interface RCTWKWebViewManager () <RCTWKWebViewDelegate>
@end
@implementation RCTWebViewManager
@implementation RCTWKWebViewManager
{
NSConditionLock *_shouldStartLoadLock;
BOOL _shouldStartLoad;
... ... @@ -28,106 +24,120 @@ RCT_EXPORT_MODULE()
- (UIView *)view
{
RCTWebView *webView = [RCTWebView new];
RCTWKWebView *webView = [RCTWKWebView new];
webView.delegate = self;
return webView;
}
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL)
RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL)
RCT_REMAP_VIEW_PROPERTY(decelerationRate, _webView.scrollView.decelerationRate, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(scalesPageToFit, BOOL)
RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
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(onMessage, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
RCT_REMAP_VIEW_PROPERTY(allowsInlineMediaPlayback, _webView.allowsInlineMediaPlayback, BOOL)
RCT_REMAP_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, _webView.mediaPlaybackRequiresUserAction, BOOL)
RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, _webView.dataDetectorTypes, UIDataDetectorTypes)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
RCT_EXPORT_VIEW_PROPERTY(allowsInlineMediaPlayback, BOOL)
RCT_EXPORT_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, BOOL)
#if WEBKIT_IOS_10_APIS_AVAILABLE
RCT_EXPORT_VIEW_PROPERTY(dataDetectorTypes, WKDataDetectorTypes)
#endif
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag)
/**
* Expose methods to enable messaging the webview.
*/
RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view goBack];
[view postMessage:message];
}
}];
}
RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag)
RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RCTWKWebView) {
view.bounces = json == nil ? true : [RCTConvert BOOL: json];
}
RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RCTWKWebView) {
view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json];
}
RCT_CUSTOM_VIEW_PROPERTY(decelerationRate, CGFloat, RCTWKWebView) {
view.decelerationRate = json == nil ? UIScrollViewDecelerationRateNormal : [RCTConvert CGFloat: json];
}
RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *)reactTag script:(NSString *)script)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view goForward];
[view injectJavaScript:script];
}
}];
}
RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag)
RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view reload];
[view goBack];
}
}];
}
RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag)
RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view stopLoading];
[view goForward];
}
}];
}
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view postMessage:message];
[view reload];
}
}];
}
RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *)reactTag script:(NSString *)script)
RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view injectJavaScript:script];
[view stopLoading];
}
}];
}
#pragma mark - Exported synchronous methods
- (BOOL)webView:(__unused RCTWebView *)webView
- (BOOL) webView:(RCTWKWebView *)webView
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
withCallback:(RCTDirectEventBlock)callback
withCallback:(RCTDirectEventBlock)callback
{
_shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()];
_shouldStartLoad = YES;
... ...
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTWebView.h"
#import <UIKit/UIKit.h>
#import "RCTAutoInsetsProtocol.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "UIView+React.h"
NSString *const RCTJSNavigationScheme = @"react-js-navigation";
static NSString *const kPostMessageHost = @"postMessage";
@interface RCTWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
@end
@implementation RCTWebView
{
UIWebView *_webView;
NSString *_injectedJavaScript;
}
- (void)dealloc
{
_webView.delegate = nil;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
super.backgroundColor = [UIColor clearColor];
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
_webView = [[UIWebView alloc] initWithFrame:self.bounds];
_webView.delegate = self;
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
_webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
#endif
[self addSubview:_webView];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)goForward
{
[_webView goForward];
}
- (void)goBack
{
[_webView goBack];
}
- (void)reload
{
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
if (request.URL && !_webView.request.URL.absoluteString.length) {
[_webView loadRequest:request];
}
else {
[_webView reload];
}
}
- (void)stopLoading
{
[_webView stopLoading];
}
- (void)postMessage:(NSString *)message
{
NSDictionary *eventInitDict = @{
@"data": message,
};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[_webView stringByEvaluatingJavaScriptFromString:source];
}
- (void)injectJavaScript:(NSString *)script
{
[_webView stringByEvaluatingJavaScriptFromString:script];
}
- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
_source = [source copy];
// Check for a static html source first
NSString *html = [RCTConvert NSString:source[@"html"]];
if (html) {
NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
if (!baseURL) {
baseURL = [NSURL URLWithString:@"about:blank"];
}
[_webView loadHTMLString:html baseURL:baseURL];
return;
}
NSURLRequest *request = [RCTConvert NSURLRequest:source];
// Because of the way React works, as pages redirect, we actually end up
// passing the redirect urls back here, so we ignore them if trying to load
// the same url. We'll expose a call to 'reload' to allow a user to load
// the existing page.
if ([request.URL isEqual:_webView.request.URL]) {
return;
}
if (!request.URL) {
// Clear the webview
[_webView loadHTMLString:@"" baseURL:nil];
return;
}
[_webView loadRequest:request];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
_webView.frame = self.bounds;
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
_contentInset = contentInset;
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:NO];
}
- (void)setScalesPageToFit:(BOOL)scalesPageToFit
{
if (_webView.scalesPageToFit != scalesPageToFit) {
_webView.scalesPageToFit = scalesPageToFit;
[_webView reload];
}
}
- (BOOL)scalesPageToFit
{
return _webView.scalesPageToFit;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
self.opaque = _webView.opaque = (alpha == 1.0);
_webView.backgroundColor = backgroundColor;
}
- (UIColor *)backgroundColor
{
return _webView.backgroundColor;
}
- (NSMutableDictionary<NSString *, id> *)baseEvent
{
NSMutableDictionary<NSString *, id> *event = [[NSMutableDictionary alloc] initWithDictionary:@{
@"url": _webView.request.URL.absoluteString ?: @"",
@"loading" : @(_webView.loading),
@"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
@"canGoBack": @(_webView.canGoBack),
@"canGoForward" : @(_webView.canGoForward),
}];
return event;
}
- (void)refreshContentInset
{
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:YES];
}
#pragma mark - UIWebViewDelegate methods
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
navigationTypes = @{
@(UIWebViewNavigationTypeLinkClicked): @"click",
@(UIWebViewNavigationTypeFormSubmitted): @"formsubmit",
@(UIWebViewNavigationTypeBackForward): @"backforward",
@(UIWebViewNavigationTypeReload): @"reload",
@(UIWebViewNavigationTypeFormResubmitted): @"formresubmit",
@(UIWebViewNavigationTypeOther): @"other",
};
});
// skip this for the JS Navigation handler
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(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];
if (isTopFrame) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": navigationTypes[@(navigationType)]
}];
_onLoadingStart(event);
}
}
if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
NSString *data = request.URL.query;
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"data": data,
}];
NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
[_webView stringByEvaluatingJavaScriptFromString:source];
_onMessage(event);
}
// JS Navigation handler
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
// ignore these since they aren't real errors.
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
return;
}
if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
// Error code 102 "Frame load interrupted" is raised by the UIWebView if
// its delegate returns FALSE from webView:shouldStartLoadWithRequest:navigationType
// when the URL is from an http redirect. This is a common pattern when
// implementing OAuth with a WebView.
return;
}
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"domain": error.domain,
@"code": @(error.code),
@"description": error.localizedDescription,
}];
_onLoadingError(event);
}
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
if (_messagingEnabled) {
#if RCT_DEV
// See isNative in lodash
NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
BOOL postMessageIsNative = [
[webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
isEqualToString:@"true"
];
if (!postMessageIsNative) {
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
#endif
NSString *source = [NSString stringWithFormat:
@"(function() {"
"window.originalPostMessage = window.postMessage;"
"var messageQueue = [];"
"var messagePending = false;"
"function processQueue() {"
"if (!messageQueue.length || messagePending) return;"
"messagePending = true;"
"window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
"}"
"window.postMessage = function(data) {"
"messageQueue.push(String(data));"
"processQueue();"
"};"
"document.addEventListener('message:received', function(e) {"
"messagePending = false;"
"processQueue();"
"});"
"})();", RCTJSNavigationScheme, kPostMessageHost
];
[webView stringByEvaluatingJavaScriptFromString:source];
}
if (_injectedJavaScript != nil) {
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
event[@"jsEvaluationValue"] = jsEvaluationValue;
_onLoadingFinish(event);
}
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) {
_onLoadingFinish([self baseEvent]);
}
}
@end