From 7a6dd9807cda45c2d60641864f2d6c8d401e8ae3 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Thu, 16 Aug 2018 13:33:58 -0700 Subject: [PATCH] Implement message passing! Summary: @public This diff implements message passing between the `WKWebView` and React Native. As with ``, we can only send/receive strings. **Usage:** 1. Set `messagingEnabled` to `true`. 1. To send data from the web view to React Native, call `postMessage(data)` within the web view. This forces React Native to execute the `onMessage` prop on the `WKWebView` component. `onMessage` will be called with an event `e`, where `e.nativeEvent.data` will be the data you passed into `postMessage`. 1. To send data from React Native to the web view, call `UIManager.dispatchViewManagerCommand` to dispatch the `UIManager.RCTWKWebView.Commands.postMessage` command. Look at [[ https://fburl.com/u1wusf2f | this part of the existing `` ]] component for more details. After you make the call, React Native will dispatch a `'message'` event to the `document` object within the webview. You can listen to the event by doing `document.addEventListener('message', callback)`. Let the event dispatched be `e`. Then, `e.data` is the data you sent over from React Native. [[ P58627181 | This Playground.js ]] illustrates the usage. Reviewed By: shergin Differential Revision: D6304850 fbshipit-source-id: 29075ef753296e9fb5a9cddeb1ad0f4ff7e28650 --- React/Views/RCTWKWebView.h | 3 ++ React/Views/RCTWKWebView.m | 67 ++++++++++++++++++++++++++++--- React/Views/RCTWKWebViewManager.h | 6 +++ React/Views/RCTWKWebViewManager.m | 25 ++++++++++-- 4 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 React/Views/RCTWKWebViewManager.h diff --git a/React/Views/RCTWKWebView.h b/React/Views/RCTWKWebView.h index 8f00bf507814d8..d094b72d4ddeeb 100644 --- a/React/Views/RCTWKWebView.h +++ b/React/Views/RCTWKWebView.h @@ -18,6 +18,9 @@ @property (nonatomic, weak) id delegate; @property (nonatomic, copy) NSDictionary *source; +@property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, copy) NSString *injectedJavaScript; +- (void)postMessage:(NSString *)message; + @end diff --git a/React/Views/RCTWKWebView.m b/React/Views/RCTWKWebView.m index ec8ba37b519130..2c9aef5e570ffd 100644 --- a/React/Views/RCTWKWebView.m +++ b/React/Views/RCTWKWebView.m @@ -1,15 +1,15 @@ #import "RCTWKWebView.h" - #import - #import - #import "RCTAutoInsetsProtocol.h" -@interface RCTWKWebView () +static NSString *const MessageHanderName = @"ReactNative"; + +@interface RCTWKWebView () @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; +@property (nonatomic, copy) RCTDirectEventBlock onMessage; @property (nonatomic, copy) WKWebView *webView; @end @@ -26,7 +26,11 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { super.backgroundColor = [UIColor clearColor]; - _webView = [[WKWebView alloc] initWithFrame:self.bounds]; + WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new]; + wkWebViewConfig.userContentController = [WKUserContentController new]; + [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName]; + + _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; _webView.UIDelegate = self; _webView.navigationDelegate = self; [self addSubview:_webView]; @@ -34,6 +38,20 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +/** + * This method is called whenever JavaScript running within the web view calls: + * - window.webkit.messageHandlers.[MessageHanderName].postMessage + */ +- (void)userContentController:(WKUserContentController *)userContentController + didReceiveScriptMessage:(WKScriptMessage *)message +{ + if (_onMessage != nil) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{@"data": message.body}]; + _onMessage(event); + } +} + - (void)setSource:(NSDictionary *)source { if (![_source isEqualToDictionary:source]) { @@ -67,9 +85,21 @@ - (void)setSource:(NSDictionary *)source } } +- (void)postMessage:(NSString *)message +{ + NSDictionary *eventInitDict = @{@"data": message}; + NSString *source = [NSString + stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", + RCTJSONStringify(eventInitDict, NULL) + ]; + [self evaluateJS: source thenCall: nil]; +} + - (void)layoutSubviews { [super layoutSubviews]; + + // Ensure webview takes the position and dimensions of RCTWKWebView _webView.frame = self.bounds; } @@ -161,7 +191,7 @@ - (void)evaluateJS:(NSString *)js thenCall: (void (^)(NSString*)) callback { [self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) { - if (error == nil) { + if (error == nil && callback != nil) { callback([NSString stringWithFormat:@"%@", result]); } }]; @@ -175,6 +205,31 @@ - (void)evaluateJS:(NSString *)js - (void) webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + if (_messagingEnabled) { + #if RCT_DEV + + // Implementation inspired by Lodash.isNative. + NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))"; + [self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) { + if (! [result isEqualToString:@"true"]) { + RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"); + } + }]; + #endif + + NSString *source = [NSString stringWithFormat: + @"(function() {" + "window.originalPostMessage = window.postMessage;" + + "window.postMessage = function(data) {" + "window.webkit.messageHandlers.%@.postMessage(String(data));" + "};" + "})();", + MessageHanderName + ]; + [self evaluateJS: source thenCall: nil]; + } + if (_injectedJavaScript) { [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) { NSMutableDictionary *event = [self baseEvent]; diff --git a/React/Views/RCTWKWebViewManager.h b/React/Views/RCTWKWebViewManager.h new file mode 100644 index 00000000000000..9099e8cf3faf26 --- /dev/null +++ b/React/Views/RCTWKWebViewManager.h @@ -0,0 +1,6 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface RCTWKWebViewManager : RCTViewManager +@end diff --git a/React/Views/RCTWKWebViewManager.m b/React/Views/RCTWKWebViewManager.m index e01198c7bae5bd..fb1db9cc3dc24f 100644 --- a/React/Views/RCTWKWebViewManager.m +++ b/React/Views/RCTWKWebViewManager.m @@ -1,8 +1,7 @@ -#import "RCTViewManager.h" -#import "RCTWKWebView.h" +#import "RCTWKWebViewManager.h" -@interface RCTWKWebViewManager : RCTViewManager -@end +#import "RCTUIManager.h" +#import "RCTWKWebView.h" @implementation RCTWKWebViewManager @@ -19,4 +18,22 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) +/** + * Expose methods to enable messaging the webview. + */ +RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) + +RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWKWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWKWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + } else { + [view postMessage:message]; + } + }]; +} + @end