From f3b70ba09aa3f5ba21323c4f33834bd32f67d944 Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sun, 13 May 2018 22:44:43 +0100 Subject: [PATCH 01/14] provisional macos support --- .gitignore | 3 + WKWebView.macos.js | 533 +++++++++++++++++ macos/RCTWKWebView.xcodeproj/project.pbxproj | 401 +++++++++++++ macos/RCTWKWebView/RCTWKWebView.h | 48 ++ macos/RCTWKWebView/RCTWKWebView.m | 559 ++++++++++++++++++ macos/RCTWKWebView/RCTWKWebViewManager.h | 10 + macos/RCTWKWebView/RCTWKWebViewManager.m | 206 +++++++ .../WKProcessPool+SharedProcessPool.h | 3 + .../WKProcessPool+SharedProcessPool.m | 16 + .../RCTWKWebView/WeakScriptMessageDelegate.h | 13 + .../RCTWKWebView/WeakScriptMessageDelegate.m | 20 + react-native-wkwebview.podspec | 8 +- 12 files changed, 1817 insertions(+), 3 deletions(-) create mode 100644 WKWebView.macos.js create mode 100644 macos/RCTWKWebView.xcodeproj/project.pbxproj create mode 100644 macos/RCTWKWebView/RCTWKWebView.h create mode 100644 macos/RCTWKWebView/RCTWKWebView.m create mode 100644 macos/RCTWKWebView/RCTWKWebViewManager.h create mode 100644 macos/RCTWKWebView/RCTWKWebViewManager.m create mode 100644 macos/RCTWKWebView/WKProcessPool+SharedProcessPool.h create mode 100644 macos/RCTWKWebView/WKProcessPool+SharedProcessPool.m create mode 100644 macos/RCTWKWebView/WeakScriptMessageDelegate.h create mode 100644 macos/RCTWKWebView/WeakScriptMessageDelegate.m diff --git a/.gitignore b/.gitignore index 9195503e..6af04a02 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ ios/RCTWKWebView.xcodeproj/xcuserdata/* ios/RCTWKWebView.xcodeproj/project.xcworkspace/xcuserdata/* + +macos/RCTWKWebView.xcodeproj/xcuserdata/* +macos/RCTWKWebView.xcodeproj/project.xcworkspace/xcuserdata/* diff --git a/WKWebView.macos.js b/WKWebView.macos.js new file mode 100644 index 00000000..f22d4770 --- /dev/null +++ b/WKWebView.macos.js @@ -0,0 +1,533 @@ +'use strict'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactNative, { + requireNativeComponent, + EdgeInsetsPropType, + StyleSheet, + UIManager, + View, + ViewPropTypes, + NativeModules, + Text, + ActivityIndicator +} from 'react-native'; + +import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; +import deprecatedPropType from 'react-native/Libraries/Utilities/deprecatedPropType'; +import invariant from 'fbjs/lib/invariant'; +import keyMirror from 'fbjs/lib/keyMirror'; +const WKWebViewManager = NativeModules.WKWebViewManager; + +var BGWASH = 'rgba(255,255,255,0.8)'; + +const WebViewState = keyMirror({ + IDLE: null, + LOADING: null, + ERROR: null, +}); + +const NavigationType = keyMirror({ + click: true, + formsubmit: true, + backforward: true, + reload: true, + formresubmit: true, + other: true, +}); + +const JSNavigationScheme = 'react-js-navigation'; + +type ErrorEvent = { + domain: any; + code: any; + description: any; +} + +type Event = Object; + +const defaultRenderLoading = () => ( + + + +); +const defaultRenderError = (errorDomain, errorCode, errorDesc) => ( + + + Error loading page + + + {'Domain: ' + errorDomain} + + + {'Error Code: ' + errorCode} + + + {'Description: ' + errorDesc} + + +); + +/** + * Renders a native WebView. + */ + +class WKWebView extends React.Component { + static JSNavigationScheme = JSNavigationScheme; + static NavigationType = NavigationType; + + static propTypes = { + ...ViewPropTypes, + + html: 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. + */ + source: PropTypes.oneOfType([ + PropTypes.shape({ + /* + * The URI to load in the WebView. Can be a local or remote file. + */ + uri: PropTypes.string, + /* + * The HTTP Method to use. Defaults to GET if not specified. + * NOTE: On Android, only GET and POST are supported. + */ + method: PropTypes.string, + /* + * Additional HTTP headers to send with the request. + * NOTE: On Android, this can only be used with GET requests. + */ + headers: PropTypes.object, + /* + * The HTTP body to send with the request. This must be a valid + * UTF-8 string, and will be sent exactly as specified, with no + * additional encoding (e.g. URL-escaping or base64) applied. + * NOTE: On Android, this can only be used with POST requests. + */ + body: PropTypes.string, + }), + PropTypes.shape({ + /* + * A static HTML page to display in the WebView. + */ + html: PropTypes.string, + /* + * The base URL to be used for any relative links in the HTML. + */ + baseUrl: PropTypes.string, + }), + /* + * Used internally by packager. + */ + PropTypes.number, + ]), + + /** + * This property specifies how the safe area insets are used to modify the + * content area of the scroll view. The default value of this property is + * "never". Available on iOS 11 and later. + */ + contentInsetAdjustmentBehavior: PropTypes.oneOf([ + 'automatic', + 'scrollableAxes', + 'never', // default + 'always', + ]), + + /** + * Function that returns a view to show if there's an error. + */ + renderError: PropTypes.func, // view to show if there's an error + /** + * Function that returns a loading indicator. + */ + renderLoading: PropTypes.func, + /** + * Invoked when load finish + */ + onLoad: PropTypes.func, + /** + * Invoked when load either succeeds or fails + */ + onLoadEnd: PropTypes.func, + /** + * Invoked on load start + */ + onLoadStart: PropTypes.func, + /** + * Invoked when load fails + */ + onError: PropTypes.func, + /** + * Report the progress + */ + onProgress: PropTypes.func, + /** + * A function that is invoked when the webview calls `window.postMessage`. + * Setting this property will inject a `postMessage` global into your + * webview, but will still call pre-existing values of `postMessage`. + * + * `window.postMessage` accepts one argument, `data`, which will be + * available on the event object, `event.nativeEvent.data`. `data` + * must be a string. + */ + onMessage: PropTypes.func, + /** + * Receive scroll events from view + */ + onScroll: PropTypes.func, + /** + * @platform ios + */ + bounces: PropTypes.bool, + scrollEnabled: PropTypes.bool, + allowsBackForwardNavigationGestures: PropTypes.bool, + automaticallyAdjustContentInsets: PropTypes.bool, + contentInset: EdgeInsetsPropType, + onNavigationStateChange: PropTypes.func, + scalesPageToFit: PropTypes.bool, + startInLoadingState: PropTypes.bool, + style: ViewPropTypes.style, + /** + * Sets the JS to be injected when the webpage loads. + */ + injectedJavaScript: PropTypes.string, + /** + * Allows custom handling of any webview requests by a JS handler. Return true + * or false from this method to continue loading the request. + * @platform ios + */ + onShouldStartLoadWithRequest: PropTypes.func, + /** + * Copies cookies from sharedHTTPCookieStorage when calling loadRequest. + * Set this to true to emulate behavior of WebView component. + */ + sendCookies: PropTypes.bool, + /** + * If set to true, target="_blank" or window.open will be opened in WebView, instead + * of new window. Default is false to be backward compatible. + */ + openNewWindowInWebView: PropTypes.bool, + /** + * Hide the accessory view when the keyboard is open. Default is false to be + * backward compatible. + */ + hideKeyboardAccessoryView: PropTypes.bool, + /** + * A Boolean value that determines whether pressing on a link displays a preview of the destination for the link. This props is available on devices that support 3D Touch. In iOS 10 and later, the default value is true; before that, the default value is false. + */ + allowsLinkPreview: PropTypes.bool, + /** + * Sets the customized user agent by using of the WKWebView + */ + customUserAgent: PropTypes.string, + userAgent: PropTypes.string, + /** + * A Boolean value that determines whether paging is enabled for the scroll view. + */ + pagingEnabled: PropTypes.bool, + /** + * A Boolean value that sets whether diagonal scrolling is allowed. + */ + directionalLockEnabled: PropTypes.bool, + }; + + state = { + viewState: WebViewState.IDLE, + lastErrorEvent: (null: ?ErrorEvent), + startInLoadingState: true, + }; + + componentWillMount() { + if (this.props.startInLoadingState) { + this.setState({ viewState: WebViewState.LOADING }); + } + } + + render() { + let otherView = null; + + if (this.state.viewState === WebViewState.LOADING) { + otherView = (this.props.renderLoading || defaultRenderLoading)(); + } else if (this.state.viewState === WebViewState.ERROR) { + 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 + ); + } else if (this.state.viewState !== WebViewState.IDLE) { + console.error( + 'RCTWKWebView invalid state encountered: ' + this.state.loading + ); + } + + 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 onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => { + const shouldStart = this.props.onShouldStartLoadWithRequest && + this.props.onShouldStartLoadWithRequest(event.nativeEvent); + WKWebViewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier); + }); + + let source = {}; + if (this.props.source && typeof this.props.source == 'object') { + source = Object.assign({}, this.props.source, { + sendCookies: this.props.sendCookies, + customUserAgent: this.props.customUserAgent || this.props.userAgent + }); + } + + if (this.props.html) { + source.html = this.props.html; + } else if (this.props.url) { + source.uri = this.props.url; + } + + const messagingEnabled = typeof this.props.onMessage === 'function'; + + const webView = + { this.webview = ref; }} + key="webViewKey" + style={webViewStyles} + contentInsetAdjustmentBehavior={this.props.contentInsetAdjustmentBehavior} + source={resolveAssetSource(source)} + injectedJavaScript={this.props.injectedJavaScript} + bounces={this.props.bounces} + scrollEnabled={this.props.scrollEnabled} + contentInset={this.props.contentInset} + allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures} + automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets} + openNewWindowInWebView={this.props.openNewWindowInWebView} + hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView} + allowsLinkPreview={this.props.allowsLinkPreview} + onLoadingStart={this._onLoadingStart} + onLoadingFinish={this._onLoadingFinish} + onLoadingError={this._onLoadingError} + messagingEnabled={messagingEnabled} + onProgress={this._onProgress} + onMessage={this._onMessage} + onScroll={this._onScroll} + onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} + pagingEnabled={this.props.pagingEnabled} + directionalLockEnabled={this.props.directionalLockEnabled} + />; + + return ( + + {webView} + {otherView} + + ); + } + + /** + * Go forward one page in the webview's history. + */ + goForward = () => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWKWebView.Commands.goForward, + null + ); + }; + + /** + * Go back one page in the webview's history. + */ + goBack = () => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWKWebView.Commands.goBack, + null + ); + }; + + /** + * Indicating whether there is a back item in the back-forward list that can be navigated to + */ + canGoBack = () => { + return WKWebViewManager.canGoBack(this.getWebViewHandle()); + }; + + /** + * Indicating whether there is a forward item in the back-forward list that can be navigated to + */ + canGoForward = () => { + return WKWebViewManager.canGoForward(this.getWebViewHandle()); + }; + + /** + * Reloads the current page. + */ + reload = () => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWKWebView.Commands.reload, + null + ); + }; + + /** + * Stop loading the current page. + */ + stopLoading = () => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWKWebView.Commands.stopLoading, + null + ) + }; + + /** + * Posts a message to the web view, which will emit a `message` event. + * Accepts one argument, `data`, which must be a string. + * + * In your webview, you'll need to something like the following. + * + * ```js + * document.addEventListener('message', e => { document.title = e.data; }); + * ``` + */ + postMessage = (data) => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWKWebView.Commands.postMessage, + [String(data)] + ); + }; + + evaluateJavaScript = (js) => { + return WKWebViewManager.evaluateJavaScript(this.getWebViewHandle(), js); + }; + + /** + * We return an event with a bunch of fields including: + * url, title, loading, canGoBack, canGoForward + */ + _updateNavigationState = (event: Event) => { + if (this.props.onNavigationStateChange) { + this.props.onNavigationStateChange(event.nativeEvent); + } + }; + + /** + * Returns the native webview node. + */ + getWebViewHandle = (): any => { + return ReactNative.findNodeHandle(this.webview); + }; + + _onLoadingStart = (event: Event) => { + 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 + 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 + }); + }; + + _onLoadingFinish = (event: Event) => { + const { onLoad, onLoadEnd } = this.props; + onLoad && onLoad(event); + onLoadEnd && onLoadEnd(event); + this.setState({ + viewState: WebViewState.IDLE, + }); + this._updateNavigationState(event); + }; + + _onProgress = (event: Event) => { + const onProgress = this.props.onProgress; + onProgress && onProgress(event.nativeEvent.progress); + }; + + _onMessage = (event: Event) => { + var { onMessage } = this.props; + onMessage && onMessage(event); + }; + + _onScroll = (event: Event) => { + const onScroll = this.props.onScroll; + onScroll && onScroll(event.nativeEvent); + }; +} + +const RCTWKWebView = requireNativeComponent('RCTWKWebView', WKWebView, { + nativeOnly: { + onLoadingStart: true, + onLoadingError: true, + onLoadingFinish: true, + onMessage: true, + messagingEnabled: PropTypes.bool, + } +}); + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: BGWASH, + }, + errorText: { + fontSize: 14, + textAlign: 'center', + marginBottom: 2, + }, + errorTextTitle: { + fontSize: 15, + fontWeight: '500', + marginBottom: 10, + }, + hidden: { + height: 0, + flex: 0, // disable 'flex:1' when hiding a View + }, + loadingView: { + backgroundColor: BGWASH, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + height: 100, + }, + webView: { + backgroundColor: '#ffffff', + } +}); + +export default WKWebView; diff --git a/macos/RCTWKWebView.xcodeproj/project.pbxproj b/macos/RCTWKWebView.xcodeproj/project.pbxproj new file mode 100644 index 00000000..df0c3d97 --- /dev/null +++ b/macos/RCTWKWebView.xcodeproj/project.pbxproj @@ -0,0 +1,401 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 097457AB1D2A457C000D9368 /* RCTWKWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 097457A91D2A457C000D9368 /* RCTWKWebViewManager.m */; }; + 097457AE1D2A4595000D9368 /* RCTWKWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 097457AD1D2A4595000D9368 /* RCTWKWebView.m */; }; + 097457AF1D2AF4E0000D9368 /* RCTWKWebView.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 097457AC1D2A4595000D9368 /* RCTWKWebView.h */; }; + 097457B01D2AF4E0000D9368 /* RCTWKWebViewManager.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 097457A81D2A457C000D9368 /* RCTWKWebViewManager.h */; }; + 3E609CF61EAA815D00187C8C /* WeakScriptMessageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E609CF51EAA815D00187C8C /* WeakScriptMessageDelegate.m */; }; + 7D8047DA20A8C8F700B3157B /* RCTWKWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 097457A91D2A457C000D9368 /* RCTWKWebViewManager.m */; }; + 7D8047DB20A8C8F700B3157B /* WeakScriptMessageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E609CF51EAA815D00187C8C /* WeakScriptMessageDelegate.m */; }; + 7D8047DC20A8C8F700B3157B /* WKProcessPool+SharedProcessPool.m in Sources */ = {isa = PBXBuildFile; fileRef = E683F3D62080F3400005F1F5 /* WKProcessPool+SharedProcessPool.m */; }; + 7D8047DD20A8C8F700B3157B /* RCTWKWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 097457AD1D2A4595000D9368 /* RCTWKWebView.m */; }; + 7D8047E020A8C8F700B3157B /* RCTWKWebView.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 097457AC1D2A4595000D9368 /* RCTWKWebView.h */; }; + 7D8047E120A8C8F700B3157B /* RCTWKWebViewManager.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 097457A81D2A457C000D9368 /* RCTWKWebViewManager.h */; }; + E683F3D72080F3400005F1F5 /* WKProcessPool+SharedProcessPool.m in Sources */ = {isa = PBXBuildFile; fileRef = E683F3D62080F3400005F1F5 /* WKProcessPool+SharedProcessPool.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 097457981D2A440A000D9368 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + 097457AF1D2AF4E0000D9368 /* RCTWKWebView.h in CopyFiles */, + 097457B01D2AF4E0000D9368 /* RCTWKWebViewManager.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8047DF20A8C8F700B3157B /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + 7D8047E020A8C8F700B3157B /* RCTWKWebView.h in CopyFiles */, + 7D8047E120A8C8F700B3157B /* RCTWKWebViewManager.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0974579A1D2A440A000D9368 /* libRCTWKWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWKWebView.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 097457A81D2A457C000D9368 /* RCTWKWebViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWKWebViewManager.h; sourceTree = ""; }; + 097457A91D2A457C000D9368 /* RCTWKWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWKWebViewManager.m; sourceTree = ""; tabWidth = 2; }; + 097457AC1D2A4595000D9368 /* RCTWKWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWKWebView.h; sourceTree = ""; }; + 097457AD1D2A4595000D9368 /* RCTWKWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RCTWKWebView.m; sourceTree = ""; tabWidth = 2; }; + 3E609CF41EAA815D00187C8C /* WeakScriptMessageDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WeakScriptMessageDelegate.h; sourceTree = ""; }; + 3E609CF51EAA815D00187C8C /* WeakScriptMessageDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WeakScriptMessageDelegate.m; sourceTree = ""; }; + 7D8047E520A8C8F700B3157B /* libRCTWKWebView-macos.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTWKWebView-macos.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E683F3D32080F2E10005F1F5 /* WKProcessPool+SharedProcessPool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WKProcessPool+SharedProcessPool.h"; sourceTree = ""; }; + E683F3D62080F3400005F1F5 /* WKProcessPool+SharedProcessPool.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "WKProcessPool+SharedProcessPool.m"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 097457971D2A440A000D9368 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8047DE20A8C8F700B3157B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 097457911D2A440A000D9368 = { + isa = PBXGroup; + children = ( + 0974579C1D2A440A000D9368 /* RCTWKWebView */, + 0974579B1D2A440A000D9368 /* Products */, + ); + sourceTree = ""; + }; + 0974579B1D2A440A000D9368 /* Products */ = { + isa = PBXGroup; + children = ( + 0974579A1D2A440A000D9368 /* libRCTWKWebView.a */, + 7D8047E520A8C8F700B3157B /* libRCTWKWebView-macos.a */, + ); + name = Products; + sourceTree = ""; + }; + 0974579C1D2A440A000D9368 /* RCTWKWebView */ = { + isa = PBXGroup; + children = ( + 097457AC1D2A4595000D9368 /* RCTWKWebView.h */, + 097457AD1D2A4595000D9368 /* RCTWKWebView.m */, + 097457A81D2A457C000D9368 /* RCTWKWebViewManager.h */, + 097457A91D2A457C000D9368 /* RCTWKWebViewManager.m */, + 3E609CF41EAA815D00187C8C /* WeakScriptMessageDelegate.h */, + 3E609CF51EAA815D00187C8C /* WeakScriptMessageDelegate.m */, + E683F3D32080F2E10005F1F5 /* WKProcessPool+SharedProcessPool.h */, + E683F3D62080F3400005F1F5 /* WKProcessPool+SharedProcessPool.m */, + ); + path = RCTWKWebView; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 097457991D2A440A000D9368 /* RCTWKWebView */ = { + isa = PBXNativeTarget; + buildConfigurationList = 097457A31D2A440A000D9368 /* Build configuration list for PBXNativeTarget "RCTWKWebView" */; + buildPhases = ( + 097457961D2A440A000D9368 /* Sources */, + 097457971D2A440A000D9368 /* Frameworks */, + 097457981D2A440A000D9368 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTWKWebView; + productName = RCTWKWebView; + productReference = 0974579A1D2A440A000D9368 /* libRCTWKWebView.a */; + productType = "com.apple.product-type.library.static"; + }; + 7D8047D820A8C8F700B3157B /* RCTWKWebView-macos */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D8047E220A8C8F700B3157B /* Build configuration list for PBXNativeTarget "RCTWKWebView-macos" */; + buildPhases = ( + 7D8047D920A8C8F700B3157B /* Sources */, + 7D8047DE20A8C8F700B3157B /* Frameworks */, + 7D8047DF20A8C8F700B3157B /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "RCTWKWebView-macos"; + productName = RCTWKWebView; + productReference = 7D8047E520A8C8F700B3157B /* libRCTWKWebView-macos.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 097457921D2A440A000D9368 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0920; + TargetAttributes = { + 097457991D2A440A000D9368 = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 097457951D2A440A000D9368 /* Build configuration list for PBXProject "RCTWKWebView" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 097457911D2A440A000D9368; + productRefGroup = 0974579B1D2A440A000D9368 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 097457991D2A440A000D9368 /* RCTWKWebView */, + 7D8047D820A8C8F700B3157B /* RCTWKWebView-macos */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 097457961D2A440A000D9368 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 097457AB1D2A457C000D9368 /* RCTWKWebViewManager.m in Sources */, + 3E609CF61EAA815D00187C8C /* WeakScriptMessageDelegate.m in Sources */, + E683F3D72080F3400005F1F5 /* WKProcessPool+SharedProcessPool.m in Sources */, + 097457AE1D2A4595000D9368 /* RCTWKWebView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8047D920A8C8F700B3157B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D8047DA20A8C8F700B3157B /* RCTWKWebViewManager.m in Sources */, + 7D8047DB20A8C8F700B3157B /* WeakScriptMessageDelegate.m in Sources */, + 7D8047DC20A8C8F700B3157B /* WKProcessPool+SharedProcessPool.m in Sources */, + 7D8047DD20A8C8F700B3157B /* RCTWKWebView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 097457A11D2A440A000D9368 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 097457A21D2A440A000D9368 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 097457A41D2A440A000D9368 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../React/**", + "$(inherited)", + "$(SRCROOT)/node_modules/react-native/React/**", + "$(SRCROOT)/../react-native/React/**", + "$(SRCROOT)/../../../node_modules/react-native/React/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 097457A51D2A440A000D9368 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../React/**", + "$(inherited)", + "$(SRCROOT)/node_modules/react-native/React/**", + "$(SRCROOT)/../react-native/React/**", + "$(SRCROOT)/../../../node_modules/react-native/React/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 7D8047E320A8C8F700B3157B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../React/**", + "$(inherited)", + "$(SRCROOT)/node_modules/react-native/React/**", + "$(SRCROOT)/../react-native/React/**", + "$(SRCROOT)/../../../node_modules/react-native/React/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 7D8047E420A8C8F700B3157B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../React/**", + "$(inherited)", + "$(SRCROOT)/node_modules/react-native/React/**", + "$(SRCROOT)/../react-native/React/**", + "$(SRCROOT)/../../../node_modules/react-native/React/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 097457951D2A440A000D9368 /* Build configuration list for PBXProject "RCTWKWebView" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 097457A11D2A440A000D9368 /* Debug */, + 097457A21D2A440A000D9368 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 097457A31D2A440A000D9368 /* Build configuration list for PBXNativeTarget "RCTWKWebView" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 097457A41D2A440A000D9368 /* Debug */, + 097457A51D2A440A000D9368 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D8047E220A8C8F700B3157B /* Build configuration list for PBXNativeTarget "RCTWKWebView-macos" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D8047E320A8C8F700B3157B /* Debug */, + 7D8047E420A8C8F700B3157B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 097457921D2A440A000D9368 /* Project object */; +} diff --git a/macos/RCTWKWebView/RCTWKWebView.h b/macos/RCTWKWebView/RCTWKWebView.h new file mode 100644 index 00000000..795646e1 --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebView.h @@ -0,0 +1,48 @@ +#import +#import +#import + +@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 RCTWKWebViewDelegate + +- (BOOL)webView:(RCTWKWebView *)webView +shouldStartLoadForRequest:(NSMutableDictionary *)request + withCallback:(RCTDirectEventBlock)callback; + +@end + +@interface RCTWKWebView : RCTView + +- (instancetype)initWithProcessPool:(WKProcessPool *)processPool; + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, copy) NSDictionary *source; +@property (nonatomic, assign) NSEdgeInsets contentInset; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, assign) BOOL messagingEnabled; +@property (nonatomic, assign) BOOL allowsLinkPreview; +@property (nonatomic, assign) BOOL openNewWindowInWebView; +@property (nonatomic, copy) NSString *injectedJavaScript; +@property (nonatomic, assign) BOOL hideKeyboardAccessoryView; + + +- (void)goForward; +- (void)goBack; +- (BOOL)canGoBack; +- (BOOL)canGoForward; +- (void)reload; +- (void)stopLoading; +- (void)postMessage:(NSString *)message; +- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *error))completionHandler; + +@end diff --git a/macos/RCTWKWebView/RCTWKWebView.m b/macos/RCTWKWebView/RCTWKWebView.m new file mode 100644 index 00000000..55d1776f --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -0,0 +1,559 @@ +#import "RCTWKWebView.h" + +#import "WeakScriptMessageDelegate.h" + +#import +// #import + +#import +#import +#import +#import +#import +#import +#import +// #import "NSView+React.h" +// #import + +#import + +// runtime trick to remove WKWebView keyboard default toolbar +// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279 +@interface _SwizzleHelperWK : NSObject @end +@implementation _SwizzleHelperWK +-(id)inputAccessoryView +{ + return nil; +} +@end + +@interface RCTWKWebView () + +@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingError; +@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; +@property (nonatomic, copy) RCTDirectEventBlock onProgress; +@property (nonatomic, copy) RCTDirectEventBlock onMessage; +@property (nonatomic, copy) RCTDirectEventBlock onScroll; +@property (assign) BOOL sendCookies; + +@end + +@implementation RCTWKWebView +{ + WKWebView *_webView; + NSString *_injectedJavaScript; +} + +- (void)reactSetFrame:(CGRect)frame +{ + [self setFrame:frame]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + return self = [super initWithFrame:frame]; +} + +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) + +- (instancetype)initWithProcessPool:(WKProcessPool *)processPool +{ + if(self = [self initWithFrame:CGRectZero]) + { + super.backgroundColor = [NSColor clearColor]; + + _automaticallyAdjustContentInsets = YES; + _contentInset = NSEdgeInsetsZero; + + WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; + config.processPool = processPool; + WKUserContentController* userController = [[WKUserContentController alloc]init]; + [userController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"reactNative"]; + config.userContentController = userController; + + _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config]; + _webView.UIDelegate = self; + _webView.navigationDelegate = self; + // _webView.scrollView.delegate = self; + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ + // `contentInsetAdjustmentBehavior` is only available since iOS 11. + // We set the default behavior to "never" so that iOS + // doesn't do weird things to UIScrollView insets automatically + // and keeps it as an opt-in behavior. + if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { + _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } +#endif + + [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil]; + [self addSubview:_webView]; + } + return self; +} + +- (void)loadRequest:(NSURLRequest *)request +{ + if (request.URL && _sendCookies) { + NSDictionary *cookies = [NSHTTPCookie requestHeaderFieldsWithCookies:[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]]; + if ([cookies objectForKey:@"Cookie"]) { + NSMutableURLRequest *mutableRequest = request.mutableCopy; + [mutableRequest addValue:cookies[@"Cookie"] forHTTPHeaderField:@"Cookie"]; + request = mutableRequest; + } + } + + [_webView loadRequest:request]; +} + +-(void)setAllowsLinkPreview:(BOOL)allowsLinkPreview +{ + if ([_webView respondsToSelector:@selector(allowsLinkPreview)]) { + _webView.allowsLinkPreview = allowsLinkPreview; + } +} + +-(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView +{ + if (!hideKeyboardAccessoryView) { + return; + } + +// UIView* subview; +// for (UIView* view in _webView.scrollView.subviews) { +// if([[view.class description] hasPrefix:@"WKContent"]) +// subview = view; +// } +// +// if(subview == nil) return; +// +// NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelperWK", subview.class.superclass]; +// Class newClass = NSClassFromString(name); +// +// if(newClass == nil) +// { +// newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0); +// if(!newClass) return; +// +// Method method = class_getInstanceMethod([_SwizzleHelperWK class], @selector(inputAccessoryView)); +// class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method)); +// +// objc_registerClassPair(newClass); +// } +// +// object_setClass(subview, newClass); +} + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +- (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior +{ + // `contentInsetAdjustmentBehavior` is available since iOS 11. + if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { + CGPoint contentOffset = _webView.scrollView.contentOffset; + _webView.scrollView.contentInsetAdjustmentBehavior = behavior; + _webView.scrollView.contentOffset = contentOffset; + } +} +#endif + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message +{ + if (_onMessage) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"data": message.body, + @"name": message.name + }]; + _onMessage(event); + } +} + +- (void)goForward +{ + [_webView goForward]; +} + +- (void)evaluateJavaScript:(NSString *)javaScriptString + completionHandler:(void (^)(id, NSError *error))completionHandler +{ + [_webView evaluateJavaScript:javaScriptString completionHandler:completionHandler]; +} + +- (void)postMessage:(NSString *)message +{ + NSDictionary *eventInitDict = @{ + @"data": message, + }; + NSString *source = [NSString + stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", + RCTJSONStringify(eventInitDict, NULL) + ]; + [_webView evaluateJavaScript:source completionHandler:nil]; +} + + +- (void)goBack +{ + [_webView goBack]; +} + +- (BOOL)canGoBack +{ + return [_webView canGoBack]; +} + +- (BOOL)canGoForward +{ + return [_webView canGoForward]; +} + +- (void)reload +{ + [_webView reload]; +} + +- (void)stopLoading +{ + [_webView stopLoading]; +} + +- (void)setSource:(NSDictionary *)source +{ + if (![_source isEqualToDictionary:source]) { + _source = [source copy]; + _sendCookies = [source[@"sendCookies"] boolValue]; + if ([source[@"customUserAgent"] length] != 0 && [_webView respondsToSelector:@selector(setCustomUserAgent:)]) { + [_webView setCustomUserAgent:source[@"customUserAgent"]]; + } + + // Allow loading local files: + // + // Only works for iOS 9+. So iOS 8 will simply ignore those two values + NSString *file = [RCTConvert NSString:source[@"file"]]; + NSString *allowingReadAccessToURL = [RCTConvert NSString:source[@"allowingReadAccessToURL"]]; + + if (file && [_webView respondsToSelector:@selector(loadFileURL:allowingReadAccessToURL:)]) { + NSURL *fileURL = [RCTConvert NSURL:file]; + NSURL *baseURL = [RCTConvert NSURL:allowingReadAccessToURL]; + [_webView loadFileURL:fileURL allowingReadAccessToURL:baseURL]; + return; + } + + // 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; + } + [self loadRequest:request]; + } +} + +- (void)layout +{ + [super layout]; + _webView.frame = self.bounds; +} + +//- (void)layoutSubviews +//{ +// [super layoutSubviews]; +// _webView.frame = self.bounds; +//} +// +//- (void)resizeSubviewsWithOldSize +//{ +// [super resizeSubviewsWithOldSize] +// print(self.bounds); +// _webView.frame = self.bounds; +//} + +- (void)setContentInset:(NSEdgeInsets)contentInset +{ + _contentInset = contentInset; +// [RCTView autoAdjustInsetsForView:self +// withScrollView:_webView.scrollView +// updateOffset:NO]; +} + +- (void)setBackgroundColor:(NSColor *)backgroundColor +{ + CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); +// self.opaque = _webView.opaque = _webView.scrollView.opaque = (alpha == 1.0); +// _webView.backgroundColor = _webView.scrollView.backgroundColor = backgroundColor; +} + +//- (NSColor *)backgroundColor +//{ +// return _webView.backgroundColor; +//} + +- (NSMutableDictionary *)baseEvent +{ + NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"url": _webView.URL.absoluteString ?: @"", + @"loading" : @(_webView.loading), + @"title": _webView.title, + @"canGoBack": @(_webView.canGoBack), + @"canGoForward" : @(_webView.canGoForward), + }]; + + return event; +} + +- (void)refreshContentInset +{ +// [RCTView autoAdjustInsetsForView:self +// withScrollView:_webView.scrollView +// updateOffset:YES]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"estimatedProgress"]) { + if (!_onProgress) { + return; + } + _onProgress(@{@"progress": [change objectForKey:NSKeyValueChangeNewKey]}); + } +} + +- (void)dealloc +{ + [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; + _webView.navigationDelegate = nil; + _webView.UIDelegate = nil; +// _webView.scrollView.delegate = nil; +} + +//- (void)scrollViewDidScroll:(UIScrollView *)scrollView +//{ +// if (!scrollView.scrollEnabled) { +// scrollView.bounds = _webView.bounds; +// return; +// } +// NSDictionary *event = @{ +// @"contentOffset": @{ +// @"x": @(scrollView.contentOffset.x), +// @"y": @(scrollView.contentOffset.y) +// }, +// @"contentInset": @{ +// @"top": @(scrollView.contentInset.top), +// @"left": @(scrollView.contentInset.left), +// @"bottom": @(scrollView.contentInset.bottom), +// @"right": @(scrollView.contentInset.right) +// }, +// @"contentSize": @{ +// @"width": @(scrollView.contentSize.width), +// @"height": @(scrollView.contentSize.height) +// }, +// @"layoutMeasurement": @{ +// @"width": @(scrollView.frame.size.width), +// @"height": @(scrollView.frame.size.height) +// }, +// @"zoomScale": @(scrollView.zoomScale ?: 1), +// }; +// _onScroll(event); +//} + +#pragma mark - WKNavigationDelegate methods + +#if DEBUG +- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { + NSURLCredential * credential = [[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust]; + completionHandler(NSURLSessionAuthChallengeUseCredential, credential); +} +#endif + +- (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSApplication *app = [NSApplication sharedApplication]; + NSURLRequest *request = navigationAction.request; + NSURL* url = request.URL; + NSString* scheme = url.scheme; + + BOOL isJSNavigation = [scheme isEqualToString:RCTJSNavigationScheme]; + +// // handle mailto and tel schemes +// if ([scheme isEqualToString:@"mailto"] || [scheme isEqualToString:@"tel"]) { +// if ([app canOpenURL:url]) { +// [app openURL:url]; +// decisionHandler(WKNavigationActionPolicyCancel); +// return; +// } +// } + + // skip this for the JS Navigation handler + if (!isJSNavigation && _onShouldStartLoadWithRequest) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": @(navigationAction.navigationType) + }]; + if (![self.delegate webView:self + shouldStartLoadForRequest:event + withCallback:_onShouldStartLoadWithRequest]) { + return decisionHandler(WKNavigationActionPolicyCancel); + } + } + + if (_onLoadingStart) { + // We have this check to filter out iframe requests and whatnot + BOOL isTopFrame = [url isEqual:request.mainDocumentURL]; + if (isTopFrame) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": url.absoluteString, + @"navigationType": @(navigationAction.navigationType) + }]; + _onLoadingStart(event); + } + } + + if (isJSNavigation) { + decisionHandler(WKNavigationActionPolicyCancel); + } + else { + decisionHandler(WKNavigationActionPolicyAllow); + } +} + +- (void)webView:(__unused WKWebView *)webView didFailProvisionalNavigation:(__unused 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 *event = [self baseEvent]; + [event addEntriesFromDictionary:@{ + @"domain": error.domain, + @"code": @(error.code), + @"description": error.localizedDescription, + }]; + _onLoadingError(event); + } +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation *)navigation +{ + if (_messagingEnabled) { +#if RCT_DEV + // See isNative in lodash + NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')"; + + [webView evaluateJavaScript:testPostMessageNative completionHandler:^(id result, NSError *error) { + if (!result) { + RCTLogWarn(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"); + } + }]; +#endif + NSString *source = @"window.originalPostMessage = window.postMessage; window.postMessage = function (data) { window.webkit.messageHandlers.reactNative.postMessage(data); }"; + [webView evaluateJavaScript:source completionHandler:nil]; + } + if (_injectedJavaScript != nil) { + [webView evaluateJavaScript:_injectedJavaScript completionHandler:^(id result, NSError *error) { + NSMutableDictionary *event = [self baseEvent]; + event[@"jsEvaluationValue"] = [NSString stringWithFormat:@"%@", result]; + _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.URL.absoluteString isEqualToString:@"about:blank"]) { + _onLoadingFinish([self baseEvent]); + } +} + +#pragma mark - WKUIDelegate + +- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { +// UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; +// +// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { +// completionHandler(); +// }]]; +// UIViewController *presentingController = RCTPresentedViewController(); +// [presentingController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { + +// // TODO We have to think message to confirm "YES" +// UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; +// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { +// completionHandler(YES); +// }]]; +// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { +// completionHandler(NO); +// }]]; +// UIViewController *presentingController = RCTPresentedViewController(); +// [presentingController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { + +// UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert]; +// [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { +// textField.text = defaultText; +// }]; +// +// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { +// NSString *input = ((UITextField *)alertController.textFields.firstObject).text; +// completionHandler(input); +// }]]; +// +// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { +// completionHandler(nil); +// }]]; +// UIViewController *presentingController = RCTPresentedViewController(); +// [presentingController presentViewController:alertController animated:YES completion:nil]; +} + +- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures +{ + NSString *scheme = navigationAction.request.URL.scheme; + if ((navigationAction.targetFrame.isMainFrame || _openNewWindowInWebView) && ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"])) { + [webView loadRequest:navigationAction.request]; + } else { + NSApplication *app = [NSApplication sharedApplication]; + NSURL *url = navigationAction.request.URL; +// if ([app canOpenURL:url]) { +// [app openURL:url]; +// } + } + return nil; +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView +{ + RCTLogWarn(@"Webview Process Terminated"); +} + +@end diff --git a/macos/RCTWKWebView/RCTWKWebViewManager.h b/macos/RCTWKWebView/RCTWKWebViewManager.h new file mode 100644 index 00000000..277f82a1 --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebViewManager.h @@ -0,0 +1,10 @@ +#import +#import + +@interface RCTConvert (UIScrollView) + +@end + +@interface RCTWKWebViewManager : RCTViewManager + +@end diff --git a/macos/RCTWKWebView/RCTWKWebViewManager.m b/macos/RCTWKWebView/RCTWKWebViewManager.m new file mode 100644 index 00000000..9d7f1076 --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebViewManager.m @@ -0,0 +1,206 @@ +#import "RCTWKWebViewManager.h" + +#import "RCTWKWebView.h" +#import "WKProcessPool+SharedProcessPool.h" +#import +#import +#import +// #import +#import + +#import + +@implementation RCTConvert (UIScrollView) + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +RCT_ENUM_CONVERTER(UIScrollViewContentInsetAdjustmentBehavior, (@{ + @"automatic": @(UIScrollViewContentInsetAdjustmentAutomatic), + @"scrollableAxes": @(UIScrollViewContentInsetAdjustmentScrollableAxes), + @"never": @(UIScrollViewContentInsetAdjustmentNever), + @"always": @(UIScrollViewContentInsetAdjustmentAlways), + }), UIScrollViewContentInsetAdjustmentNever, integerValue) +#endif + +@end + +@interface RCTWKWebViewManager () + +@end + +@implementation RCTWKWebViewManager +{ + NSConditionLock *_shouldStartLoadLock; + BOOL _shouldStartLoad; +} + +RCT_EXPORT_MODULE() + +- (NSView *)view +{ + RCTWKWebView *webView = [[RCTWKWebView alloc] initWithProcessPool:[WKProcessPool sharedProcessPool]]; + webView.delegate = self; + return webView; +} + +RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) +RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL) +RCT_REMAP_VIEW_PROPERTY(pagingEnabled, _webView.scrollView.pagingEnabled, BOOL) +RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) +RCT_REMAP_VIEW_PROPERTY(directionalLockEnabled, _webView.scrollView.directionalLockEnabled, BOOL) +RCT_REMAP_VIEW_PROPERTY(allowsBackForwardNavigationGestures, _webView.allowsBackForwardNavigationGestures, BOOL) +RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) +RCT_EXPORT_VIEW_PROPERTY(openNewWindowInWebView, BOOL) +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(onShouldStartLoadWithRequest, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL) +RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL) +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) +#endif + +RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWKWebView, got: %@", view); + } else { + [view goBack]; + } + }]; +} + +RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWKWebView, got: %@", view); + } else { + [view goForward]; + } + }]; +} + +RCT_EXPORT_METHOD(canGoBack:(nonnull NSNumber *)reactTag + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + + resolve([NSNumber numberWithBool:[view canGoBack]]); + }]; +} + +RCT_EXPORT_METHOD(canGoForward:(nonnull NSNumber *)reactTag + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + + resolve([NSNumber numberWithBool:[view canGoForward]]); + }]; +} + +RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWKWebView, got: %@", view); + } else { + [view reload]; + } + }]; +} + +RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWKWebView, got: %@", view); + } else { + [view stopLoading]; + } + }]; +} + +RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + } else { + [view postMessage:message]; + } + }]; +} + +RCT_EXPORT_METHOD(evaluateJavaScript:(nonnull NSNumber *)reactTag + js:(NSString *)js + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWKWebView, got: %@", view); + } else { + [view evaluateJavaScript:js completionHandler:^(id result, NSError *error) { + if (error) { + reject(@"js_error", @"Error occurred while evaluating Javascript", error); + } else { + resolve(result); + } + }]; + } + }]; +} + +#pragma mark - Exported synchronous methods + +- (BOOL)webView:(__unused RCTWKWebView *)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 diff --git a/macos/RCTWKWebView/WKProcessPool+SharedProcessPool.h b/macos/RCTWKWebView/WKProcessPool+SharedProcessPool.h new file mode 100644 index 00000000..e3a5e2f1 --- /dev/null +++ b/macos/RCTWKWebView/WKProcessPool+SharedProcessPool.h @@ -0,0 +1,3 @@ +@interface WKProcessPool (SharedProcessPool) ++ (WKProcessPool*)sharedProcessPool; +@end diff --git a/macos/RCTWKWebView/WKProcessPool+SharedProcessPool.m b/macos/RCTWKWebView/WKProcessPool+SharedProcessPool.m new file mode 100644 index 00000000..eec006f7 --- /dev/null +++ b/macos/RCTWKWebView/WKProcessPool+SharedProcessPool.m @@ -0,0 +1,16 @@ +#import +#import +#import "WKProcessPool+SharedProcessPool.h" + +@implementation WKProcessPool (SharedProcessPool) + ++ (WKProcessPool*)sharedProcessPool { + static WKProcessPool* _sharedProcessPool; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedProcessPool = [[WKProcessPool alloc] init]; + }); + return _sharedProcessPool; +} + +@end diff --git a/macos/RCTWKWebView/WeakScriptMessageDelegate.h b/macos/RCTWKWebView/WeakScriptMessageDelegate.h new file mode 100644 index 00000000..dd676a0c --- /dev/null +++ b/macos/RCTWKWebView/WeakScriptMessageDelegate.h @@ -0,0 +1,13 @@ + +#import +#import + +// Trampoline object to avoid retain cycle with the script message handler +@interface WeakScriptMessageDelegate : NSObject + +@property (nonatomic, weak) id scriptDelegate; + +- (instancetype)initWithDelegate:(id)scriptDelegate; + +@end + diff --git a/macos/RCTWKWebView/WeakScriptMessageDelegate.m b/macos/RCTWKWebView/WeakScriptMessageDelegate.m new file mode 100644 index 00000000..11a79174 --- /dev/null +++ b/macos/RCTWKWebView/WeakScriptMessageDelegate.m @@ -0,0 +1,20 @@ + +#import "WeakScriptMessageDelegate.h" + +@implementation WeakScriptMessageDelegate + +- (instancetype)initWithDelegate:(id)scriptDelegate +{ + self = [super init]; + if (self) { + _scriptDelegate = scriptDelegate; + } + return self; +} + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message +{ + [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +@end diff --git a/react-native-wkwebview.podspec b/react-native-wkwebview.podspec index acfd65d2..bdf92be2 100644 --- a/react-native-wkwebview.podspec +++ b/react-native-wkwebview.podspec @@ -5,17 +5,19 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "react-native-wkwebview" s.version = package["version"] - s.summary = "React Native WKWebView for iOS" + s.summary = "React Native WKWebView for iOS and macOS" s.author = "Ruoyu Sun (https://github.com/insraq)" s.homepage = "https://github.com/CRAlpha/react-native-wkwebview" s.license = "MIT" - s.platform = :ios, "8.0" + s.ios.deployment_target = "8.0" + s.osx.deployment_target = "10.12" s.source = { :git => "https://github.com/CRAlpha/react-native-wkwebview.git", :tag => "v#{s.version}" } - s.source_files = "ios/RCTWKWebView/*.{h,m}" + s.ios.source_files = "ios/RCTWKWebView/*.{h,m}" + s.osx.source_files = "macos/RCTWKWebView/*.{h,m}" s.dependency "React" end From 45c5f3699976d67ca87ec5a70bf6bc258ce71dec Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sun, 13 May 2018 23:02:13 +0100 Subject: [PATCH 02/14] Minimum deployment target: 10.10 (earliest that WKWebView is available for macOS) --- macos/RCTWKWebView.xcodeproj/project.pbxproj | 2 ++ package.json | 5 +++-- react-native-wkwebview.podspec | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/macos/RCTWKWebView.xcodeproj/project.pbxproj b/macos/RCTWKWebView.xcodeproj/project.pbxproj index df0c3d97..3f88db6c 100644 --- a/macos/RCTWKWebView.xcodeproj/project.pbxproj +++ b/macos/RCTWKWebView.xcodeproj/project.pbxproj @@ -341,6 +341,7 @@ "$(SRCROOT)/../react-native/React/**", "$(SRCROOT)/../../../node_modules/react-native/React/**", ); + MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -358,6 +359,7 @@ "$(SRCROOT)/../react-native/React/**", "$(SRCROOT)/../../../node_modules/react-native/React/**", ); + MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; diff --git a/package.json b/package.json index d0a79872..0187d742 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "native", "wkwebview", "webview", - "ios" + "ios", + "macos" ], "license": "MIT", "main": "index.js", @@ -77,5 +78,5 @@ "sync-from-example": "cp ./example/node_modules/react-native-wkwebview-reborn/*.js ./;cp -r ./example/node_modules/react-native-wkwebview-reborn/ios ./", "sync-to-example": "cp ./*.js ./example/node_modules/react-native-wkwebview-reborn/;cp -r ./ios ./example/node_modules/react-native-wkwebview-reborn/" }, - "version": "1.17.0" + "version": "1.20.0" } diff --git a/react-native-wkwebview.podspec b/react-native-wkwebview.podspec index bdf92be2..8d0a9570 100644 --- a/react-native-wkwebview.podspec +++ b/react-native-wkwebview.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.license = "MIT" s.ios.deployment_target = "8.0" - s.osx.deployment_target = "10.12" + s.osx.deployment_target = "10.10" s.source = { :git => "https://github.com/CRAlpha/react-native-wkwebview.git", :tag => "v#{s.version}" } From 56f874741dc2f77ef17c0214e144985ae9e2073f Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sun, 13 May 2018 23:42:28 +0100 Subject: [PATCH 03/14] support for all JavaScript alert panel APIs. --- README.md | 27 +++++++++--- .../contents.xcworkspacedata | 7 +++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++ macos/RCTWKWebView/RCTWKWebView.m | 43 +++++++++++++++++++ package.json | 2 +- 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 macos/RCTWKWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 macos/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/README.md b/README.md index a2587c48..c99863d4 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,24 @@ [![npm version](https://badge.fury.io/js/react-native-wkwebview-reborn.svg)](https://badge.fury.io/js/react-native-wkwebview-reborn) -React Native comes with [WebView](http://facebook.github.io/react-native/docs/webview.html) component, which uses UIWebView on iOS. This component uses [WKWebView](http://nshipster.com/wkwebkit/) introduced in iOS 8 with all the performance boost. +React Native comes with [WebView](http://facebook.github.io/react-native/docs/webview.html) component, which uses UIWebView on iOS. This component uses [WKWebView](http://nshipster.com/wkwebkit/) introduced in iOS 8.0 and macOS 10.10 with all the performance boosts. **Deployment Target >= iOS 8.0 is required** *(which is React Native's current minimum deployment target anyway).* +**Deployment Target >= macOS 10.10 is required**. ### Install -1. Install from npm (note the postfix in the package name): `npm install react-native-wkwebview-reborn` -2. run `react-native link react-native-wkwebview-reborn` +1. Install from npm (note the postfix in the package name): `yarn add https://github.com/shirakaba/react-native-wkwebview.git#macos` +2. run `react-native link react-native-wkwebview-reborn` (WARNING: untested for macOS!) **Manual alternative** -1. Install from npm (note the postfix in the package name): `npm install react-native-wkwebview-reborn` +1. Install from npm (note the postfix in the package name): `yarn add https://github.com/shirakaba/react-native-wkwebview.git#macos` 2. In the XCode's "Project navigator", right click on your project's Libraries folder ➜ Add Files to <...> -3. Go to node_modules ➜ react-native-wkwebview-reborn ➜ ios ➜ select `RCTWKWebView.xcodeproj` -4. Go your build target ➜ Build Phases ➜ Link Binary With Libraries, click "+" and select `libRCTWkWebView.a` (see the following screenshot for reference) +3. Go to node_modules ➜ react-native-wkwebview-reborn ➜ ios (or macos) ➜ select `RCTWKWebView.xcodeproj` +4. Go your build target ➜ Build Phases ➜ Link Binary With Libraries, click "+" and select `libRCTWkWebView.a` (or `libRCTWkWebView-macos.a`) (see the following screenshot for reference) ![Linking](https://user-images.githubusercontent.com/608221/28060167-0650e3f4-6659-11e7-8085-7a8c2615f90f.png) -5. Compile and profit (Remember to set Minimum Deployment Target = 8.0) +5. Compile and profit (Remember to set `Minimum Deployment Target` to 8.0 for iOS, or 10.10 for macOS) ### Usage @@ -114,6 +115,18 @@ This property specifies how the safe area insets are used to modify the content - allowsInlineMediaPlayback - decelerationRate +#### Incomplete APIs for macOS + +Some iOS methods have not been fully implemented on macOS yet. This is because the macOS implementation of `WKWebView` has some differences, such as lacking a `scrollView`, using `AppKit` instead of `UIKit`, and lacking any equivalent to `UIAlertController`; and so I don't quite know what should be done in the macOS case. I therefore can't guarantee the success of the following methods: + +- `setHideKeyboardAccessoryView` +- `setContentInset` +- `setBackgroundColor` +- `refreshContentInset` +- `scrollViewDidScroll` + +`mailto` and `tel` schemes are also not supported yet because I don't know the macOS equivalent. + ### Advanced Communication between React Native and WkWebView diff --git a/macos/RCTWKWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/macos/RCTWKWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..b2ae6140 --- /dev/null +++ b/macos/RCTWKWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/macos/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/RCTWKWebView/RCTWKWebView.m b/macos/RCTWKWebView/RCTWKWebView.m index 55d1776f..3afa759a 100644 --- a/macos/RCTWKWebView/RCTWKWebView.m +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -494,6 +494,16 @@ - (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation #pragma mark - WKUIDelegate - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:NSAlertStyleWarning]; + [alert addButtonWithTitle:@"Close"]; + [alert setMessageText:@"JavaScript error"]; + [alert setInformativeText:message]; + [alert setAlertStyle:NSAlertStyleWarning]; + [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { + if(returnCode == NSAlertFirstButtonReturn) completionHandler(); + }]; + // UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; // // [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { @@ -504,6 +514,20 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin } - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:NSAlertStyleWarning]; + [alert addButtonWithTitle:@"OK"]; // first + [alert addButtonWithTitle:@"Cancel"]; // second + [alert setMessageText:@"JavaScript confirmation"]; + [alert setInformativeText:message]; + [alert setAlertStyle:NSAlertStyleWarning]; + [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { + if(returnCode == NSAlertFirstButtonReturn){ + completionHandler(YES); + } else if(returnCode == NSAlertSecondButtonReturn){ + completionHandler(NO); + } + }]; // // TODO We have to think message to confirm "YES" // UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; @@ -518,6 +542,25 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr } - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:NSAlertStyleWarning]; + [alert addButtonWithTitle:@"OK"]; + [alert addButtonWithTitle:@"Cancel"]; + [alert setMessageText:prompt]; + [alert setAlertStyle:NSAlertStyleWarning]; + + NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)]; + [input setStringValue:defaultText]; + [alert setAccessoryView:input]; + + [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { + if(returnCode == NSAlertFirstButtonReturn){ + NSString *userInput = [input stringValue]; + completionHandler(userInput); + } else if(returnCode == NSAlertSecondButtonReturn){ + completionHandler(nil); + } + }]; // UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert]; // [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { diff --git a/package.json b/package.json index 0187d742..44ac1b0c 100644 --- a/package.json +++ b/package.json @@ -78,5 +78,5 @@ "sync-from-example": "cp ./example/node_modules/react-native-wkwebview-reborn/*.js ./;cp -r ./example/node_modules/react-native-wkwebview-reborn/ios ./", "sync-to-example": "cp ./*.js ./example/node_modules/react-native-wkwebview-reborn/;cp -r ./ios ./example/node_modules/react-native-wkwebview-reborn/" }, - "version": "1.20.0" + "version": "1.20.1" } From f5d14feb867301b12377a53157f31e995a3ff910 Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 22:38:59 +0100 Subject: [PATCH 04/14] Added comments regarding UIKit -> AppKit decisions and removed export of scrollView properties. --- macos/RCTWKWebView/RCTWKWebView.m | 87 +++++++----------------- macos/RCTWKWebView/RCTWKWebViewManager.m | 34 ++++----- 2 files changed, 42 insertions(+), 79 deletions(-) diff --git a/macos/RCTWKWebView/RCTWKWebView.m b/macos/RCTWKWebView/RCTWKWebView.m index 3afa759a..f3ab3f8a 100644 --- a/macos/RCTWKWebView/RCTWKWebView.m +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -3,7 +3,6 @@ #import "WeakScriptMessageDelegate.h" #import -// #import #import #import @@ -12,8 +11,6 @@ #import #import #import -// #import "NSView+React.h" -// #import #import @@ -76,17 +73,19 @@ - (instancetype)initWithProcessPool:(WKProcessPool *)processPool _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config]; _webView.UIDelegate = self; _webView.navigationDelegate = self; - // _webView.scrollView.delegate = self; - -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - // `contentInsetAdjustmentBehavior` is only available since iOS 11. - // We set the default behavior to "never" so that iOS - // doesn't do weird things to UIScrollView insets automatically - // and keeps it as an opt-in behavior. - if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { - _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } -#endif + +/* Removed because macOS WKWebView doesn't have a scrollView */ +// _webView.scrollView.delegate = self; +// +//#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +// // `contentInsetAdjustmentBehavior` is only available since iOS 11. +// // We set the default behavior to "never" so that iOS +// // doesn't do weird things to UIScrollView insets automatically +// // and keeps it as an opt-in behavior. +// if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { +// _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; +// } +//#endif [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil]; [self addSubview:_webView]; @@ -121,6 +120,7 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView return; } +/* Removed because macOS WKWebView doesn't have a scrollView */ // UIView* subview; // for (UIView* view in _webView.scrollView.subviews) { // if([[view.class description] hasPrefix:@"WKContent"]) @@ -269,28 +269,17 @@ - (void)setSource:(NSDictionary *)source } } +/* My understanding of AppKit's equivalent to UIKit's layoutSubviews() */ - (void)layout { [super layout]; _webView.frame = self.bounds; } -//- (void)layoutSubviews -//{ -// [super layoutSubviews]; -// _webView.frame = self.bounds; -//} -// -//- (void)resizeSubviewsWithOldSize -//{ -// [super resizeSubviewsWithOldSize] -// print(self.bounds); -// _webView.frame = self.bounds; -//} - - (void)setContentInset:(NSEdgeInsets)contentInset { _contentInset = contentInset; +/* Removed because macOS WKWebView doesn't have a scrollView */ // [RCTView autoAdjustInsetsForView:self // withScrollView:_webView.scrollView // updateOffset:NO]; @@ -299,10 +288,12 @@ - (void)setContentInset:(NSEdgeInsets)contentInset - (void)setBackgroundColor:(NSColor *)backgroundColor { CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); +/* Removed because macOS WKWebView doesn't have a scrollView */ // self.opaque = _webView.opaque = _webView.scrollView.opaque = (alpha == 1.0); // _webView.backgroundColor = _webView.scrollView.backgroundColor = backgroundColor; } +/* Removed because macOS WKWebView doesn't have a backgroundColor */ //- (NSColor *)backgroundColor //{ // return _webView.backgroundColor; @@ -323,6 +314,7 @@ - (void)setBackgroundColor:(NSColor *)backgroundColor - (void)refreshContentInset { +/* Removed because macOS WKWebView doesn't have a scrollView */ // [RCTView autoAdjustInsetsForView:self // withScrollView:_webView.scrollView // updateOffset:YES]; @@ -346,9 +338,11 @@ - (void)dealloc [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; _webView.navigationDelegate = nil; _webView.UIDelegate = nil; + /* Removed because macOS WKWebView doesn't have a scrollView */ // _webView.scrollView.delegate = nil; } +/* Removed because macOS WKWebView doesn't have a scrollView */ //- (void)scrollViewDidScroll:(UIScrollView *)scrollView //{ // if (!scrollView.scrollEnabled) { @@ -397,7 +391,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W BOOL isJSNavigation = [scheme isEqualToString:RCTJSNavigationScheme]; -// // handle mailto and tel schemes +// TODO: handle mailto and tel schemes // if ([scheme isEqualToString:@"mailto"] || [scheme isEqualToString:@"tel"]) { // if ([app canOpenURL:url]) { // [app openURL:url]; @@ -503,17 +497,10 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin [alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { if(returnCode == NSAlertFirstButtonReturn) completionHandler(); }]; - -// UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; -// -// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -// completionHandler(); -// }]]; -// UIViewController *presentingController = RCTPresentedViewController(); -// [presentingController presentViewController:alertController animated:YES completion:nil]; } - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler { + // TODO We have to think message to confirm "YES" NSAlert *alert = [[NSAlert alloc] init]; [alert setAlertStyle:NSAlertStyleWarning]; [alert addButtonWithTitle:@"OK"]; // first @@ -528,17 +515,6 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr completionHandler(NO); } }]; - -// // TODO We have to think message to confirm "YES" -// UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert]; -// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -// completionHandler(YES); -// }]]; -// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -// completionHandler(NO); -// }]]; -// UIViewController *presentingController = RCTPresentedViewController(); -// [presentingController presentViewController:alertController animated:YES completion:nil]; } - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler { @@ -561,22 +537,6 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt completionHandler(nil); } }]; - -// UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert]; -// [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { -// textField.text = defaultText; -// }]; -// -// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -// NSString *input = ((UITextField *)alertController.textFields.firstObject).text; -// completionHandler(input); -// }]]; -// -// [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -// completionHandler(nil); -// }]]; -// UIViewController *presentingController = RCTPresentedViewController(); -// [presentingController presentViewController:alertController animated:YES completion:nil]; } - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures @@ -587,6 +547,7 @@ - (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWe } else { NSApplication *app = [NSApplication sharedApplication]; NSURL *url = navigationAction.request.URL; + // TODO: // if ([app canOpenURL:url]) { // [app openURL:url]; // } diff --git a/macos/RCTWKWebView/RCTWKWebViewManager.m b/macos/RCTWKWebView/RCTWKWebViewManager.m index 9d7f1076..0fc2981e 100644 --- a/macos/RCTWKWebView/RCTWKWebViewManager.m +++ b/macos/RCTWKWebView/RCTWKWebViewManager.m @@ -5,21 +5,21 @@ #import #import #import -// #import #import #import @implementation RCTConvert (UIScrollView) -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ -RCT_ENUM_CONVERTER(UIScrollViewContentInsetAdjustmentBehavior, (@{ - @"automatic": @(UIScrollViewContentInsetAdjustmentAutomatic), - @"scrollableAxes": @(UIScrollViewContentInsetAdjustmentScrollableAxes), - @"never": @(UIScrollViewContentInsetAdjustmentNever), - @"always": @(UIScrollViewContentInsetAdjustmentAlways), - }), UIScrollViewContentInsetAdjustmentNever, integerValue) -#endif +/* Removed because macOS WKWebView doesn't have a scrollView */ +//#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +//RCT_ENUM_CONVERTER(UIScrollViewContentInsetAdjustmentBehavior, (@{ +// @"automatic": @(UIScrollViewContentInsetAdjustmentAutomatic), +// @"scrollableAxes": @(UIScrollViewContentInsetAdjustmentScrollableAxes), +// @"never": @(UIScrollViewContentInsetAdjustmentNever), +// @"always": @(UIScrollViewContentInsetAdjustmentAlways), +// }), UIScrollViewContentInsetAdjustmentNever, integerValue) +//#endif @end @@ -43,10 +43,11 @@ - (NSView *)view } RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) -RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL) -RCT_REMAP_VIEW_PROPERTY(pagingEnabled, _webView.scrollView.pagingEnabled, BOOL) -RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) -RCT_REMAP_VIEW_PROPERTY(directionalLockEnabled, _webView.scrollView.directionalLockEnabled, BOOL) +/* Removed because macOS WKWebView doesn't have a scrollView */ +//RCT_REMAP_VIEW_PROPERTY(bounces, _webView.scrollView.bounces, BOOL) +//RCT_REMAP_VIEW_PROPERTY(pagingEnabled, _webView.scrollView.pagingEnabled, BOOL) +//RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) +//RCT_REMAP_VIEW_PROPERTY(directionalLockEnabled, _webView.scrollView.directionalLockEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(allowsBackForwardNavigationGestures, _webView.allowsBackForwardNavigationGestures, BOOL) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) RCT_EXPORT_VIEW_PROPERTY(openNewWindowInWebView, BOOL) @@ -62,9 +63,10 @@ - (NSView *)view RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL) RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL) -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ -RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) -#endif +/* Removed because macOS WKWebView doesn't have a scrollView */ +//#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +//RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior) +//#endif RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) { From 7ef42374295ee39e20174006250202c42b16dea9 Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 22:54:42 +0100 Subject: [PATCH 05/14] Update readme.md for pull request. --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c99863d4..6a069fa8 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,20 @@ React Native comes with [WebView](http://facebook.github.io/react-native/docs/webview.html) component, which uses UIWebView on iOS. This component uses [WKWebView](http://nshipster.com/wkwebkit/) introduced in iOS 8.0 and macOS 10.10 with all the performance boosts. -**Deployment Target >= iOS 8.0 is required** *(which is React Native's current minimum deployment target anyway).* -**Deployment Target >= macOS 10.10 is required**. +* **Deployment Target >= iOS 8.0 is required** *(which is React Native's current minimum deployment target anyway)* when deploying to iOS. +* **Deployment Target >= macOS 10.10 is required** when deploying to macOS. ### Install -1. Install from npm (note the postfix in the package name): `yarn add https://github.com/shirakaba/react-native-wkwebview.git#macos` -2. run `react-native link react-native-wkwebview-reborn` (WARNING: untested for macOS!) +1. Install from npm (note the postfix in the package name): `npm install react-native-wkwebview-reborn` +2. run `react-native link react-native-wkwebview-reborn` (beware: automatic linking is untested for macOS!) **Manual alternative** -1. Install from npm (note the postfix in the package name): `yarn add https://github.com/shirakaba/react-native-wkwebview.git#macos` +1. Install from npm (note the postfix in the package name): `npm install react-native-wkwebview-reborn` 2. In the XCode's "Project navigator", right click on your project's Libraries folder ➜ Add Files to <...> 3. Go to node_modules ➜ react-native-wkwebview-reborn ➜ ios (or macos) ➜ select `RCTWKWebView.xcodeproj` -4. Go your build target ➜ Build Phases ➜ Link Binary With Libraries, click "+" and select `libRCTWkWebView.a` (or `libRCTWkWebView-macos.a`) (see the following screenshot for reference) +4. Go your build target ➜ Build Phases ➜ Link Binary With Libraries, click "+" and select `libRCTWkWebView.a` (or `libRCTWkWebView-macos.a`, when building for macOS) (see the following screenshot for reference) ![Linking](https://user-images.githubusercontent.com/608221/28060167-0650e3f4-6659-11e7-8085-7a8c2615f90f.png) 5. Compile and profit (Remember to set `Minimum Deployment Target` to 8.0 for iOS, or 10.10 for macOS) @@ -117,7 +117,7 @@ This property specifies how the safe area insets are used to modify the content #### Incomplete APIs for macOS -Some iOS methods have not been fully implemented on macOS yet. This is because the macOS implementation of `WKWebView` has some differences, such as lacking a `scrollView`, using `AppKit` instead of `UIKit`, and lacking any equivalent to `UIAlertController`; and so I don't quite know what should be done in the macOS case. I therefore can't guarantee the success of the following methods: +Some iOS methods have not been fully implemented for macOS yet. This is because the macOS implementation of `WKWebView` has some differences, such as lacking a `scrollView`, and using `AppKit` instead of `UIKit`; and so I (@shirakaba) don't quite know what should be done in the macOS case. I therefore can't guarantee the success of the following methods for macOS: - `setHideKeyboardAccessoryView` - `setContentInset` From 75d8c1c5f8d156d7d4684b126ac1dbf8664a7c32 Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 22:56:26 +0100 Subject: [PATCH 06/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a069fa8..d68c792e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ React Native comes with [WebView](http://facebook.github.io/react-native/docs/we ### Install 1. Install from npm (note the postfix in the package name): `npm install react-native-wkwebview-reborn` -2. run `react-native link react-native-wkwebview-reborn` (beware: automatic linking is untested for macOS!) +2. run `react-native link react-native-wkwebview-reborn` **Manual alternative** From 32ff52110f5bbd277c2b37fce7b02b38a8831c9a Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 22:58:28 +0100 Subject: [PATCH 07/14] update from master --- WKWebView.ios.js | 1 + WKWebView.macos.js | 1 + 2 files changed, 2 insertions(+) diff --git a/WKWebView.ios.js b/WKWebView.ios.js index f22d4770..b1f03903 100644 --- a/WKWebView.ios.js +++ b/WKWebView.ios.js @@ -382,6 +382,7 @@ class WKWebView extends React.Component { * Reloads the current page. */ reload = () => { + this.setState({ viewState: WebViewState.LOADING }); UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWKWebView.Commands.reload, diff --git a/WKWebView.macos.js b/WKWebView.macos.js index f22d4770..b1f03903 100644 --- a/WKWebView.macos.js +++ b/WKWebView.macos.js @@ -382,6 +382,7 @@ class WKWebView extends React.Component { * Reloads the current page. */ reload = () => { + this.setState({ viewState: WebViewState.LOADING }); UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWKWebView.Commands.reload, From 0326627c4fe4f404d3eb2a5032414620334b3903 Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 23:43:10 +0100 Subject: [PATCH 08/14] xcshareddata --- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ios/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/ios/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/RCTWKWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + From 5e4d5429b4cf9a9e13795541cc3ba2907976de7b Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 23:55:10 +0100 Subject: [PATCH 09/14] port injectJavaScript to macOS --- WKWebView.macos.js | 17 ++++++++ macos/RCTWKWebView/RCTWKWebView.h | 3 ++ macos/RCTWKWebView/RCTWKWebView.m | 54 ++++++++++++++++++++---- macos/RCTWKWebView/RCTWKWebViewManager.m | 7 ++- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/WKWebView.macos.js b/WKWebView.macos.js index b1f03903..fb0248c4 100644 --- a/WKWebView.macos.js +++ b/WKWebView.macos.js @@ -199,6 +199,20 @@ class WKWebView extends React.Component { scalesPageToFit: PropTypes.bool, startInLoadingState: PropTypes.bool, style: ViewPropTypes.style, + /** + * If false injectJavaScript will run both main frame and iframe + * @platform macos + */ + injectJavaScriptForMainFrameOnly: PropTypes.bool, + /** + * If false injectedJavaScript will run both main frame and iframe + * @platform macos + */ + injectedJavaScriptForMainFrameOnly: PropTypes.bool, + /** + * Function that accepts a string that will be passed to the WebView and executed immediately as JavaScript. + */ + injectJavaScript: PropTypes.string, /** * Sets the JS to be injected when the webpage loads. */ @@ -313,6 +327,9 @@ class WKWebView extends React.Component { style={webViewStyles} contentInsetAdjustmentBehavior={this.props.contentInsetAdjustmentBehavior} source={resolveAssetSource(source)} + injectJavaScriptForMainFrameOnly={this.props.injectJavaScriptForMainFrameOnly} + injectedJavaScriptForMainFrameOnly={this.props.injectedJavaScriptForMainFrameOnly} + injectJavaScript={this.props.injectJavaScript} injectedJavaScript={this.props.injectedJavaScript} bounces={this.props.bounces} scrollEnabled={this.props.scrollEnabled} diff --git a/macos/RCTWKWebView/RCTWKWebView.h b/macos/RCTWKWebView/RCTWKWebView.h index 795646e1..a12e11f4 100644 --- a/macos/RCTWKWebView/RCTWKWebView.h +++ b/macos/RCTWKWebView/RCTWKWebView.h @@ -32,6 +32,9 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, assign) BOOL allowsLinkPreview; @property (nonatomic, assign) BOOL openNewWindowInWebView; +@property (nonatomic, assign) BOOL injectJavaScriptForMainFrameOnly; +@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly; +@property (nonatomic, copy) NSString *injectJavaScript; @property (nonatomic, copy) NSString *injectedJavaScript; @property (nonatomic, assign) BOOL hideKeyboardAccessoryView; diff --git a/macos/RCTWKWebView/RCTWKWebView.m b/macos/RCTWKWebView/RCTWKWebView.m index f3ab3f8a..2026f7bf 100644 --- a/macos/RCTWKWebView/RCTWKWebView.m +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -34,12 +34,17 @@ @interface RCTWKWebView () *event = [self baseEvent]; - event[@"jsEvaluationValue"] = [NSString stringWithFormat:@"%@", result]; - _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.URL.absoluteString isEqualToString:@"about:blank"]) { + if (_onLoadingFinish && !webView.loading && ![webView.URL.absoluteString isEqualToString:@"about:blank"]) { _onLoadingFinish([self baseEvent]); } } diff --git a/macos/RCTWKWebView/RCTWKWebViewManager.m b/macos/RCTWKWebView/RCTWKWebViewManager.m index 0fc2981e..4a1a36ad 100644 --- a/macos/RCTWKWebView/RCTWKWebViewManager.m +++ b/macos/RCTWKWebView/RCTWKWebViewManager.m @@ -49,6 +49,9 @@ - (NSView *)view //RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) //RCT_REMAP_VIEW_PROPERTY(directionalLockEnabled, _webView.scrollView.directionalLockEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(allowsBackForwardNavigationGestures, _webView.allowsBackForwardNavigationGestures, BOOL) +RCT_EXPORT_VIEW_PROPERTY(injectJavaScriptForMainFrameOnly, BOOL) +RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL) +RCT_EXPORT_VIEW_PROPERTY(injectJavaScript, NSString) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) RCT_EXPORT_VIEW_PROPERTY(openNewWindowInWebView, BOOL) RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) @@ -98,7 +101,7 @@ - (NSView *)view { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTWKWebView *view = viewRegistry[reactTag]; - + resolve([NSNumber numberWithBool:[view canGoBack]]); }]; } @@ -109,7 +112,7 @@ - (NSView *)view { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTWKWebView *view = viewRegistry[reactTag]; - + resolve([NSNumber numberWithBool:[view canGoForward]]); }]; } From 926e2e87291debd567e3fe4819d629a91bb34124 Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Wed, 16 May 2018 23:55:21 +0100 Subject: [PATCH 10/14] platform: ios -> macos --- WKWebView.macos.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WKWebView.macos.js b/WKWebView.macos.js index fb0248c4..1842a16e 100644 --- a/WKWebView.macos.js +++ b/WKWebView.macos.js @@ -188,7 +188,7 @@ class WKWebView extends React.Component { */ onScroll: PropTypes.func, /** - * @platform ios + * @platform macos */ bounces: PropTypes.bool, scrollEnabled: PropTypes.bool, @@ -220,7 +220,7 @@ class WKWebView extends React.Component { /** * Allows custom handling of any webview requests by a JS handler. Return true * or false from this method to continue loading the request. - * @platform ios + * @platform macos */ onShouldStartLoadWithRequest: PropTypes.func, /** From 8646a67d230a7d3ddf716559293ac0649b6fcd52 Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Thu, 31 May 2018 20:24:50 +0100 Subject: [PATCH 11/14] Version incremented --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0187d742..2f1404e5 100644 --- a/package.json +++ b/package.json @@ -78,5 +78,5 @@ "sync-from-example": "cp ./example/node_modules/react-native-wkwebview-reborn/*.js ./;cp -r ./example/node_modules/react-native-wkwebview-reborn/ios ./", "sync-to-example": "cp ./*.js ./example/node_modules/react-native-wkwebview-reborn/;cp -r ./ios ./example/node_modules/react-native-wkwebview-reborn/" }, - "version": "1.20.0" + "version": "1.20.2" } From c8a7df348a29478b5a3cde97ea3d82706a7fe5ec Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sun, 8 Jul 2018 18:26:18 +0100 Subject: [PATCH 12/14] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9195503e..985f0581 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ ios/RCTWKWebView.xcodeproj/xcuserdata/* ios/RCTWKWebView.xcodeproj/project.xcworkspace/xcuserdata/* +macos/RCTWKWebView.xcodeproj/xcuserdata From eb877d4866cb1cb5453e76b29a5a1afba0dffd6a Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sun, 8 Jul 2018 18:36:28 +0100 Subject: [PATCH 13/14] Merge iOS setMessagingEnabled() bugfix into macOS --- macos/RCTWKWebView/RCTWKWebView.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/macos/RCTWKWebView/RCTWKWebView.m b/macos/RCTWKWebView/RCTWKWebView.m index a7adb3d8..083cde7b 100644 --- a/macos/RCTWKWebView/RCTWKWebView.m +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -128,6 +128,11 @@ - (void)setInjectJavaScriptForMainFrameOnly:(BOOL)injectJavaScriptForMainFrameOn } } +- (void)setMessagingEnabled:(BOOL)messagingEnabled { + _messagingEnabled = messagingEnabled; + [self setupPostMessageScript]; +} + - (void)resetupScripts { [_webView.configuration.userContentController removeAllUserScripts]; [self setupPostMessageScript]; From 8cb5248b4dc30b02304096398f50e02e82fe636a Mon Sep 17 00:00:00 2001 From: shirakaba <14055146+shirakaba@users.noreply.github.com> Date: Sat, 25 Aug 2018 22:03:00 +0100 Subject: [PATCH 14/14] pinch-to-zoom --- macos/RCTWKWebView/RCTWKWebView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/macos/RCTWKWebView/RCTWKWebView.m b/macos/RCTWKWebView/RCTWKWebView.m index 083cde7b..c1d539eb 100644 --- a/macos/RCTWKWebView/RCTWKWebView.m +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -76,6 +76,7 @@ - (instancetype)initWithProcessPool:(WKProcessPool *)processPool config.userContentController = userController; _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config]; + _webView.allowsMagnification = YES; // macOS-only _webView.UIDelegate = self; _webView.navigationDelegate = self;