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/README.md b/README.md index ee5e2833..66b4320b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ [![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 >= 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 @@ -15,10 +16,10 @@ React Native comes with [WebView](http://facebook.github.io/react-native/docs/we 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 ➜ 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`, 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 = 8.0) +5. Compile and profit (Remember to set `Minimum Deployment Target` to 8.0 for iOS, or 10.10 for macOS) ### Usage @@ -127,6 +128,18 @@ Add JavaScript at document end. Since 1.20.0, the implementation has been change - allowsInlineMediaPlayback - decelerationRate +#### Incomplete APIs for macOS + +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` +- `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/WKWebView.macos.js b/WKWebView.macos.js new file mode 100644 index 00000000..f42b839c --- /dev/null +++ b/WKWebView.macos.js @@ -0,0 +1,558 @@ +'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 macos + */ + 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, + /** + * 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. + */ + 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 macos + */ + 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, + /** + * Enable the keyboard to display when focusing an input in a webview programatically. + * [Removed because macOS WKWebView doesn't have an on-screen keyboard] + */ + // keyboardDisplayRequiresUserAction: 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)} + injectJavaScriptForMainFrameOnly={this.props.injectJavaScriptForMainFrameOnly} + injectedJavaScriptForMainFrameOnly={this.props.injectedJavaScriptForMainFrameOnly} + injectJavaScript={this.props.injectJavaScript} + 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} + /* Removed because macOS doesn't have an on-screen keyboard. */ + // keyboardDisplayRequiresUserAction={this.props.keyboardDisplayRequiresUserAction} + 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 = () => { + this.setState({ viewState: WebViewState.LOADING }); + 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/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 + + + diff --git a/ios/RCTWKWebView/RCTWKWebView.h b/ios/RCTWKWebView/RCTWKWebView.h index 1327bdfd..0c208dc4 100644 --- a/ios/RCTWKWebView/RCTWKWebView.h +++ b/ios/RCTWKWebView/RCTWKWebView.h @@ -37,7 +37,7 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, copy) NSString *injectJavaScript; @property (nonatomic, copy) NSString *injectedJavaScript; @property (nonatomic, assign) BOOL hideKeyboardAccessoryView; -@property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; +// @property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; /* macOS doesn't have an on-screen keyboard */ - (void)goForward; diff --git a/macos/RCTWKWebView.xcodeproj/project.pbxproj b/macos/RCTWKWebView.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3f88db6c --- /dev/null +++ b/macos/RCTWKWebView.xcodeproj/project.pbxproj @@ -0,0 +1,403 @@ +// !$*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/**", + ); + MACOSX_DEPLOYMENT_TARGET = 10.10; + 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/**", + ); + MACOSX_DEPLOYMENT_TARGET = 10.10; + 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.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.h b/macos/RCTWKWebView/RCTWKWebView.h new file mode 100644 index 00000000..e98b831d --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebView.h @@ -0,0 +1,52 @@ +#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, assign) BOOL injectJavaScriptForMainFrameOnly; +@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly; +@property (nonatomic, copy) NSString *injectJavaScript; +@property (nonatomic, copy) NSString *injectedJavaScript; +@property (nonatomic, assign) BOOL hideKeyboardAccessoryView; +@property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; + + +- (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..c1d539eb --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebView.m @@ -0,0 +1,633 @@ +#import "RCTWKWebView.h" + +#import "WeakScriptMessageDelegate.h" + +#import + +#import +#import +#import +#import +#import +#import +#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; +@property (nonatomic, strong) WKUserScript *atStartScript; +@property (nonatomic, strong) WKUserScript *atEndScript; + +@end + +@implementation RCTWKWebView +{ + WKWebView *_webView; + BOOL _injectJavaScriptForMainFrameOnly; + BOOL _injectedJavaScriptForMainFrameOnly; + NSString *_injectJavaScript; + 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.allowsMagnification = YES; // macOS-only + _webView.UIDelegate = self; + _webView.navigationDelegate = self; + +/* 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 + [self setupPostMessageScript]; + [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil]; + [self addSubview:_webView]; + } + return self; +} + +- (void)setInjectJavaScript:(NSString *)injectJavaScript { + _injectJavaScript = injectJavaScript; + self.atStartScript = [[WKUserScript alloc] initWithSource:injectJavaScript + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:_injectJavaScriptForMainFrameOnly]; + [self resetupScripts]; +} + +- (void)setInjectedJavaScript:(NSString *)script { + _injectedJavaScript = script; + self.atEndScript = [[WKUserScript alloc] initWithSource:script + injectionTime:WKUserScriptInjectionTimeAtDocumentEnd + forMainFrameOnly:_injectedJavaScriptForMainFrameOnly]; + [self resetupScripts]; +} + +- (void)setInjectedJavaScriptForMainFrameOnly:(BOOL)injectedJavaScriptForMainFrameOnly { + _injectedJavaScriptForMainFrameOnly = injectedJavaScriptForMainFrameOnly; + if (_injectedJavaScript != nil) { + [self setInjectedJavaScript:_injectedJavaScript]; + } +} + +- (void)setInjectJavaScriptForMainFrameOnly:(BOOL)injectJavaScriptForMainFrameOnly { + _injectJavaScriptForMainFrameOnly = injectJavaScriptForMainFrameOnly; + if (_injectJavaScript != nil) { + [self setInjectJavaScript:_injectJavaScript]; + } +} + +- (void)setMessagingEnabled:(BOOL)messagingEnabled { + _messagingEnabled = messagingEnabled; + [self setupPostMessageScript]; +} + +- (void)resetupScripts { + [_webView.configuration.userContentController removeAllUserScripts]; + [self setupPostMessageScript]; + if (self.atStartScript) { + [_webView.configuration.userContentController addUserScript:self.atStartScript]; + } + if (self.atEndScript) { + [_webView.configuration.userContentController addUserScript:self.atEndScript]; + } +} + +- (void)setupPostMessageScript { + if (_messagingEnabled) { + NSString *source=@"window.originalPostMessage = window.postMessage; window.postMessage = function (data) { window.webkit.messageHandlers.reactNative.postMessage(data); }"; + WKUserScript *script = [[WKUserScript alloc] initWithSource:source + injectionTime:WKUserScriptInjectionTimeAtDocumentEnd + forMainFrameOnly:_injectedJavaScriptForMainFrameOnly]; + [_webView.configuration.userContentController addUserScript:script]; + } +} + +- (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; + } + + /* Removed because macOS WKWebView doesn't have a scrollView */ + // 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); +} + +/* Removed because macOS WKWebView doesn't have an on-screen keyboard */ +// // https://github.com/Telerik-Verified-Plugins/WKWebView/commit/04e8296adeb61f289f9c698045c19b62d080c7e3 +// // https://stackoverflow.com/a/48623286/3297914 +// -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAction +// { +// if (!keyboardDisplayRequiresUserAction) { +// Class class = NSClassFromString(@"WKContentView"); +// NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0}; + +// if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) { +// SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"); +// Method method = class_getInstanceMethod(class, selector); +// IMP original = method_getImplementation(method); +// IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { +// ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); +// }); +// method_setImplementation(method, override); +// } else { +// SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:"); +// Method method = class_getInstanceMethod(class, selector); +// IMP original = method_getImplementation(method); +// IMP override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) { +// ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); +// }); +// method_setImplementation(method, override); +// } +// } +// } + +#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]; + } +} + +/* My understanding of AppKit's equivalent to UIKit's layoutSubviews() */ +- (void)layout +{ + [super layout]; + _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]; +} + +- (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; +//} + +- (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 +{ +/* Removed because macOS WKWebView doesn't have a scrollView */ +// [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; + /* 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) { +// 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]; + +// TODO: 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 +{ + // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. + 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 { + 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(); + }]; +} + +- (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 + [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); + } + }]; +} + +- (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); + } + }]; +} + +- (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; + // TODO: +// 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..179f0019 --- /dev/null +++ b/macos/RCTWKWebView/RCTWKWebViewManager.m @@ -0,0 +1,213 @@ +#import "RCTWKWebViewManager.h" + +#import "RCTWKWebView.h" +#import "WKProcessPool+SharedProcessPool.h" +#import +#import +#import +#import + +#import + +@implementation RCTConvert (UIScrollView) + +/* 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 + +@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) +/* 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(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) +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) +/* Removed because macOS doesn't have an on-screen keyboard */ +// RCT_EXPORT_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL) +RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL) +/* 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) +{ + [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/package.json b/package.json index d8bbaa39..a6f8bcd6 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.21.0" + "version": "1.21.1" } diff --git a/react-native-wkwebview.podspec b/react-native-wkwebview.podspec index acfd65d2..8d0a9570 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.10" 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