/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RNCWKWebView.h" #import #import #import "RNCWKProcessPoolManager.h" #import #import "objc/runtime.h" static NSString *const MessageHanderName = @"ReactNative"; // 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 RNCWKWebView () @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; @property (nonatomic, copy) RCTDirectEventBlock onLoadingProgress; @property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; @property (nonatomic, copy) RCTDirectEventBlock onMessage; @property (nonatomic, copy) WKWebView *webView; @end @implementation RNCWKWebView { UIColor * _savedBackgroundColor; BOOL _savedHideKeyboardAccessoryView; } - (void)dealloc{} /** * See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html. */ + (BOOL)dynamicallyLoadWebKitIfAvailable { static BOOL _webkitAvailable=NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSBundle *webKitBundle = [NSBundle bundleWithPath:@"/System/Library/Frameworks/WebKit.framework"]; if (webKitBundle) { _webkitAvailable = [webKitBundle load]; } }); return _webkitAvailable; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { super.backgroundColor = [UIColor clearColor]; _bounces = YES; _scrollEnabled = YES; _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; } return self; } - (void)didMoveToWindow { if (self.window != nil && _webView == nil) { if (![[self class] dynamicallyLoadWebKitIfAvailable]) { return; }; WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new]; if (_incognito) { wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; } if(self.useSharedProcessPool) { wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool]; } wkWebViewConfig.userContentController = [WKUserContentController new]; [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName]; wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback; #if WEBKIT_IOS_10_APIS_AVAILABLE wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction ? WKAudiovisualMediaTypeAll : WKAudiovisualMediaTypeNone; wkWebViewConfig.dataDetectorTypes = _dataDetectorTypes; #else wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction; #endif _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; _webView.scrollView.delegate = self; _webView.UIDelegate = self; _webView.navigationDelegate = self; _webView.scrollView.scrollEnabled = _scrollEnabled; _webView.scrollView.pagingEnabled = _pagingEnabled; _webView.scrollView.bounces = _bounces; _webView.allowsLinkPreview = _allowsLinkPreview; [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures; if (_userAgent) { _webView.customUserAgent = _userAgent; } #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } #endif [self addSubview:_webView]; [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView]; [self visitSource]; } } // Update webview property when the component prop changes. - (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigationGestures { _allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures; _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures; } - (void)removeFromSuperview { if (_webView) { [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName]; [_webView removeObserver:self forKeyPath:@"estimatedProgress"]; [_webView removeFromSuperview]; _webView = nil; } [super removeFromSuperview]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) { if(_onLoadingProgress){ NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary:@{@"progress":[NSNumber numberWithDouble:self.webView.estimatedProgress]}]; _onLoadingProgress(event); } }else{ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)setBackgroundColor:(UIColor *)backgroundColor { _savedBackgroundColor = backgroundColor; if (_webView == nil) { return; } CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); self.opaque = _webView.opaque = (alpha == 1.0); _webView.scrollView.backgroundColor = backgroundColor; _webView.backgroundColor = backgroundColor; } /** * This method is called whenever JavaScript running within the web view calls: * - window.webkit.messageHandlers.[MessageHanderName].postMessage */ - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if (_onMessage != nil) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{@"data": message.body}]; _onMessage(event); } } - (void)setSource:(NSDictionary *)source { if (![_source isEqualToDictionary:source]) { _source = [source copy]; if (_webView != nil) { [self visitSource]; } } } - (void)setContentInset:(UIEdgeInsets)contentInset { _contentInset = contentInset; [RCTView autoAdjustInsetsForView:self withScrollView:_webView.scrollView updateOffset:NO]; } - (void)refreshContentInset { [RCTView autoAdjustInsetsForView:self withScrollView:_webView.scrollView updateOffset:YES]; } - (void)visitSource { // Check for a static html source first NSString *html = [RCTConvert NSString:_source[@"html"]]; if (html) { NSURL *baseURL = [RCTConvert NSURL:_source[@"baseUrl"]]; if (!baseURL) { baseURL = [NSURL URLWithString:@"about:blank"]; } [_webView loadHTMLString:html baseURL:baseURL]; return; } NSURLRequest *request = [RCTConvert NSURLRequest:_source]; // Because of the way React works, as pages redirect, we actually end up // passing the redirect urls back here, so we ignore them if trying to load // the same url. We'll expose a call to 'reload' to allow a user to load // the existing page. if ([request.URL isEqual:_webView.URL]) { return; } if (!request.URL) { // Clear the webview [_webView loadHTMLString:@"" baseURL:nil]; return; } [_webView loadRequest:request]; } -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView { if (_webView == nil) { _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView; return; } if (_savedHideKeyboardAccessoryView == false) { return; } UIView* subview; for (UIView* view in _webView.scrollView.subviews) { if([[view.class description] hasPrefix:@"WK"]) 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); } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { scrollView.decelerationRate = _decelerationRate; } - (void)setScrollEnabled:(BOOL)scrollEnabled { _scrollEnabled = scrollEnabled; _webView.scrollView.scrollEnabled = scrollEnabled; } - (void)postMessage:(NSString *)message { NSDictionary *eventInitDict = @{@"data": message}; NSString *source = [NSString stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", RCTJSONStringify(eventInitDict, NULL) ]; [self evaluateJS: source thenCall: nil]; } - (void)layoutSubviews { [super layoutSubviews]; // Ensure webview takes the position and dimensions of RNCWKWebView _webView.frame = self.bounds; } - (NSMutableDictionary *)baseEvent { NSDictionary *event = @{ @"url": _webView.URL.absoluteString ?: @"", @"title": _webView.title, @"loading" : @(_webView.loading), @"canGoBack": @(_webView.canGoBack), @"canGoForward" : @(_webView.canGoForward) }; return [[NSMutableDictionary alloc] initWithDictionary: event]; } #pragma mark - WKNavigationDelegate methods /** * alert */ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { completionHandler(); }]]; [[self topViewController] presentViewController:alert animated:YES completion:NULL]; } /** * confirm */ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { completionHandler(YES); }]]; [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completionHandler(NO); }]]; [[self topViewController] presentViewController:alert animated:YES completion:NULL]; } /** * prompt */ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.textColor = [UIColor lightGrayColor]; textField.placeholder = defaultText; }]; [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { completionHandler([[alert.textFields lastObject] text]); }]]; [[self topViewController] presentViewController:alert animated:YES completion:NULL]; } /** * topViewController */ -(UIViewController *)topViewController{    UIViewController *controller = [self topViewControllerWithRootViewController:[self getCurrentWindow].rootViewController];    return controller; } /** * topViewControllerWithRootViewController */ -(UIViewController *)topViewControllerWithRootViewController:(UIViewController *)viewController{ if (viewController==nil) return nil; if (viewController.presentedViewController!=nil) { return [self topViewControllerWithRootViewController:viewController.presentedViewController]; } else if ([viewController isKindOfClass:[UITabBarController class]]){ return [self topViewControllerWithRootViewController:[(UITabBarController *)viewController selectedViewController]]; } else if ([viewController isKindOfClass:[UINavigationController class]]){ return [self topViewControllerWithRootViewController:[(UINavigationController *)viewController visibleViewController]]; } else { return viewController; } } /** * getCurrentWindow */ -(UIWindow *)getCurrentWindow{ UIWindow *window = [UIApplication sharedApplication].keyWindow; if (window.windowLevel!=UIWindowLevelNormal) { for (UIWindow *wid in [UIApplication sharedApplication].windows) { if (window.windowLevel==UIWindowLevelNormal) { window = wid; break; } } } return window; } /** * Decides whether to allow or cancel a navigation. * @see https://fburl.com/42r9fxob */ - (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { static NSDictionary *navigationTypes; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ navigationTypes = @{ @(WKNavigationTypeLinkActivated): @"click", @(WKNavigationTypeFormSubmitted): @"formsubmit", @(WKNavigationTypeBackForward): @"backforward", @(WKNavigationTypeReload): @"reload", @(WKNavigationTypeFormResubmitted): @"formresubmit", @(WKNavigationTypeOther): @"other", }; }); WKNavigationType navigationType = navigationAction.navigationType; NSURLRequest *request = navigationAction.request; if (_onShouldStartLoadWithRequest) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ @"url": (request.URL).absoluteString, @"navigationType": navigationTypes[@(navigationType)] }]; if (![self.delegate webView:self shouldStartLoadForRequest:event withCallback:_onShouldStartLoadWithRequest]) { decisionHandler(WKNavigationResponsePolicyCancel); return; } } if (_onLoadingStart) { // We have this check to filter out iframe requests and whatnot BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; if (isTopFrame) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ @"url": (request.URL).absoluteString, @"navigationType": navigationTypes[@(navigationType)] }]; _onLoadingStart(event); } } // Allow all navigation by default decisionHandler(WKNavigationResponsePolicyAllow); } /** * Called when an error occurs while the web view is loading content. * @see https://fburl.com/km6vqenw */ - (void) webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error { if (_onLoadingError) { if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { // NSURLErrorCancelled is reported when a page has a redirect OR if you load // a new URL in the WebView before the previous one came back. We can just // ignore these since they aren't real errors. // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os return; } if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) { // Error code 102 "Frame load interrupted" is raised by the WKWebView // when the URL is from an http redirect. This is a common pattern when // implementing OAuth with a WebView. return; } NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary:@{ @"didFailProvisionalNavigation": @YES, @"domain": error.domain, @"code": @(error.code), @"description": error.localizedDescription, }]; _onLoadingError(event); } [self setBackgroundColor: _savedBackgroundColor]; } - (void)evaluateJS:(NSString *)js thenCall: (void (^)(NSString*)) callback { [self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) { if (error == nil) { if (callback != nil) { callback([NSString stringWithFormat:@"%@", result]); } } else { RCTLogError(@"Error evaluating injectedJavaScript: This is possibly due to an unsupported return type. Try adding true to the end of your injectedJavaScript string."); } }]; } /** * Called when the navigation is complete. * @see https://fburl.com/rtys6jlb */ - (void) webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { if (_messagingEnabled) { #if RCT_DEV // Implementation inspired by Lodash.isNative. NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))"; [self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) { if (! [result isEqualToString:@"true"]) { RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"); } }]; #endif NSString *source = [NSString stringWithFormat: @"(function() {" "window.originalPostMessage = window.postMessage;" "window.postMessage = function(data) {" "window.webkit.messageHandlers.%@.postMessage(String(data));" "};" "})();", MessageHanderName ]; [self evaluateJS: source thenCall: nil]; } if (_injectedJavaScript) { [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) { NSMutableDictionary *event = [self baseEvent]; event[@"jsEvaluationValue"] = jsEvaluationValue; if (self.onLoadingFinish) { self.onLoadingFinish(event); } }]; } else if (_onLoadingFinish) { _onLoadingFinish([self baseEvent]); } [self setBackgroundColor: _savedBackgroundColor]; } - (void)injectJavaScript:(NSString *)script { [self evaluateJS: script thenCall: nil]; } - (void)goForward { [_webView goForward]; } - (void)goBack { [_webView goBack]; } - (void)reload { /** * When the initial load fails due to network connectivity issues, * [_webView reload] doesn't reload the webpage. Therefore, we must * manually call [_webView loadRequest:request]. */ NSURLRequest *request = [RCTConvert NSURLRequest:self.source]; if (request.URL && !_webView.URL.absoluteString.length) { [_webView loadRequest:request]; } else { [_webView reload]; } } - (void)stopLoading { [_webView stopLoading]; } - (void)setBounces:(BOOL)bounces { _bounces = bounces; _webView.scrollView.bounces = bounces; } @end