mirror of
https://github.com/status-im/react-native-webview-bridge.git
synced 2026-08-31 05:11:12 +00:00
396 lines
12 KiB
Objective-C
396 lines
12 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "RCTWebViewBridge.h"
|
|
|
|
#import <UIKit/UIKit.h>
|
|
#import <objc/runtime.h>
|
|
|
|
#import "RCTAutoInsetsProtocol.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTUtils.h"
|
|
#import "RCTView.h"
|
|
#import "UIView+React.h"
|
|
|
|
//This is a very elegent way of defining multiline string in objective-c.
|
|
//source: http://stackoverflow.com/a/23387659/828487
|
|
#define NSStringMultiline(...) [[NSString alloc] initWithCString:#__VA_ARGS__ encoding:NSUTF8StringEncoding]
|
|
|
|
// runtime trick to remove UIWebview keyboard default toolbar
|
|
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
|
|
@interface _SwizzleHelper : NSObject @end
|
|
@implementation _SwizzleHelper
|
|
-(id)inputAccessoryView
|
|
{
|
|
return nil;
|
|
}
|
|
@end
|
|
|
|
static const NSString* RCTWebViewBridgeSchema = @"rnwb";
|
|
|
|
@interface RCTWebViewBridge () <UIWebViewDelegate, UIScrollViewDelegate, RCTAutoInsetsProtocol>
|
|
|
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
|
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
|
|
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
|
|
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
|
|
@property (nonatomic, copy) RCTDirectEventBlock onBridgeMessage;
|
|
@property (nonatomic, copy) RCTDirectEventBlock onStatusBarTap;
|
|
|
|
@end
|
|
|
|
@implementation RCTWebViewBridge
|
|
{
|
|
UIWebView *_webView;
|
|
NSString *_injectedJavaScript;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
_webView.delegate = nil;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
if ((self = [super initWithFrame:frame])) {
|
|
super.backgroundColor = [UIColor clearColor];
|
|
_automaticallyAdjustContentInsets = YES;
|
|
_contentInset = UIEdgeInsetsZero;
|
|
_webView = [[UIWebView alloc] initWithFrame:self.bounds];
|
|
_webView.delegate = self;
|
|
[self addSubview:_webView];
|
|
|
|
UIScrollView *_fakeScrollView = [[UIScrollView alloc] initWithFrame:UIScreen.mainScreen.bounds];
|
|
_fakeScrollView.delegate = self;
|
|
_fakeScrollView.scrollsToTop = YES;
|
|
[self addSubview:_fakeScrollView];
|
|
[self sendSubviewToBack:_fakeScrollView];
|
|
_fakeScrollView.contentSize = CGSizeMake(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height + 1.0f);
|
|
_fakeScrollView.contentOffset = CGPointMake(0.0f, UIScreen.mainScreen.bounds.size.height);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
|
|
{
|
|
if (_onStatusBarTap) {
|
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
|
_onStatusBarTap(event);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|
|
|
- (void)goForward
|
|
{
|
|
[_webView goForward];
|
|
}
|
|
|
|
- (void)goBack
|
|
{
|
|
[_webView goBack];
|
|
}
|
|
|
|
- (void)reload
|
|
{
|
|
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
|
|
if (request.URL && !_webView.request.URL.absoluteString.length) {
|
|
[_webView loadRequest:request];
|
|
}
|
|
else {
|
|
[_webView reload];
|
|
}
|
|
}
|
|
|
|
- (void)stopLoading
|
|
{
|
|
[_webView stopLoading];
|
|
}
|
|
|
|
- (void)closeKeyboard
|
|
{
|
|
[_webView endEditing:YES];
|
|
}
|
|
|
|
- (void)setSource:(NSDictionary *)source
|
|
{
|
|
if (![_source isEqualToDictionary:source]) {
|
|
_source = [source copy];
|
|
|
|
// Check for a static html source first
|
|
NSString *html = [RCTConvert NSString:source[@"html"]];
|
|
if (html) {
|
|
NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
|
|
if (!baseURL) {
|
|
baseURL = [NSURL URLWithString:@"about:blank"];
|
|
}
|
|
[_webView loadHTMLString:html baseURL:baseURL];
|
|
return;
|
|
}
|
|
|
|
NSURLRequest *request = [RCTConvert NSURLRequest:source];
|
|
// Because of the way React works, as pages redirect, we actually end up
|
|
// passing the redirect urls back here, so we ignore them if trying to load
|
|
// the same url. We'll expose a call to 'reload' to allow a user to load
|
|
// the existing page.
|
|
if ([request.URL isEqual:_webView.request.URL]) {
|
|
return;
|
|
}
|
|
if (!request.URL) {
|
|
// Clear the webview
|
|
[_webView loadHTMLString:@"" baseURL:nil];
|
|
return;
|
|
}
|
|
[_webView loadRequest:request];
|
|
}
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
[super layoutSubviews];
|
|
_webView.frame = self.bounds;
|
|
}
|
|
|
|
- (void)setContentInset:(UIEdgeInsets)contentInset
|
|
{
|
|
_contentInset = contentInset;
|
|
[RCTView autoAdjustInsetsForView:self
|
|
withScrollView:_webView.scrollView
|
|
updateOffset:NO];
|
|
}
|
|
|
|
- (void)setScalesPageToFit:(BOOL)scalesPageToFit
|
|
{
|
|
if (_webView.scalesPageToFit != scalesPageToFit) {
|
|
_webView.scalesPageToFit = scalesPageToFit;
|
|
[_webView reload];
|
|
}
|
|
}
|
|
|
|
- (BOOL)scalesPageToFit
|
|
{
|
|
return _webView.scalesPageToFit;
|
|
}
|
|
|
|
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
|
{
|
|
CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor);
|
|
self.opaque = _webView.opaque = (alpha == 1.0);
|
|
_webView.backgroundColor = backgroundColor;
|
|
}
|
|
|
|
- (UIColor *)backgroundColor
|
|
{
|
|
return _webView.backgroundColor;
|
|
}
|
|
|
|
- (NSMutableDictionary<NSString *, id> *)baseEvent
|
|
{
|
|
NSMutableDictionary<NSString *, id> *event = [[NSMutableDictionary alloc] initWithDictionary:@{
|
|
@"url": _webView.request.URL.absoluteString ?: @"",
|
|
@"loading" : @(_webView.loading),
|
|
@"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"],
|
|
@"canGoBack": @(_webView.canGoBack),
|
|
@"canGoForward" : @(_webView.canGoForward),
|
|
}];
|
|
|
|
return event;
|
|
}
|
|
|
|
- (void)refreshContentInset
|
|
{
|
|
[RCTView autoAdjustInsetsForView:self
|
|
withScrollView:_webView.scrollView
|
|
updateOffset:YES];
|
|
}
|
|
|
|
#pragma mark - UIWebViewDelegate methods
|
|
|
|
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
|
|
navigationType:(UIWebViewNavigationType)navigationType
|
|
{
|
|
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
|
|
|
|
if (!isJSNavigation && [request.URL.scheme isEqualToString:RCTWebViewBridgeSchema]) {
|
|
NSString* message = [webView stringByEvaluatingJavaScriptFromString:@"WebViewBridge.__fetch__()"];
|
|
|
|
NSMutableDictionary<NSString *, id> *onBridgeMessageEvent = [[NSMutableDictionary alloc] initWithDictionary:@{
|
|
@"messages": [self stringArrayJsonToArray: message]
|
|
}];
|
|
|
|
_onBridgeMessage(onBridgeMessageEvent);
|
|
|
|
isJSNavigation = YES;
|
|
}
|
|
|
|
static NSDictionary<NSNumber *, NSString *> *navigationTypes;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
navigationTypes = @{
|
|
@(UIWebViewNavigationTypeLinkClicked): @"click",
|
|
@(UIWebViewNavigationTypeFormSubmitted): @"formsubmit",
|
|
@(UIWebViewNavigationTypeBackForward): @"backforward",
|
|
@(UIWebViewNavigationTypeReload): @"reload",
|
|
@(UIWebViewNavigationTypeFormResubmitted): @"formresubmit",
|
|
@(UIWebViewNavigationTypeOther): @"other",
|
|
};
|
|
});
|
|
|
|
// skip this for the JS Navigation handler
|
|
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
|
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
|
[event addEntriesFromDictionary: @{
|
|
@"url": (request.URL).absoluteString,
|
|
@"navigationType": navigationTypes[@(navigationType)]
|
|
}];
|
|
if (![self.delegate webView:self
|
|
shouldStartLoadForRequest:event
|
|
withCallback:_onShouldStartLoadWithRequest]) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (_onLoadingStart) {
|
|
// We have this check to filter out iframe requests and whatnot
|
|
BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL];
|
|
if (isTopFrame) {
|
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
|
[event addEntriesFromDictionary: @{
|
|
@"url": (request.URL).absoluteString,
|
|
@"navigationType": navigationTypes[@(navigationType)]
|
|
}];
|
|
_onLoadingStart(event);
|
|
}
|
|
}
|
|
|
|
// JS Navigation handler
|
|
return !isJSNavigation;
|
|
}
|
|
|
|
- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error
|
|
{
|
|
if (_onLoadingError) {
|
|
if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
|
|
// NSURLErrorCancelled is reported when a page has a redirect OR if you load
|
|
// a new URL in the WebView before the previous one came back. We can just
|
|
// ignore these since they aren't real errors.
|
|
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
|
|
return;
|
|
}
|
|
|
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
|
[event addEntriesFromDictionary:@{
|
|
@"domain": error.domain,
|
|
@"code": @(error.code),
|
|
@"description": error.localizedDescription,
|
|
}];
|
|
_onLoadingError(event);
|
|
}
|
|
}
|
|
|
|
- (void)webViewDidFinishLoad:(UIWebView *)webView
|
|
{
|
|
//injecting WebViewBridge bootstrap
|
|
[webView stringByEvaluatingJavaScriptFromString:[self webViewBridgeBootrstrap]];
|
|
|
|
//injecting WebViewBridgeRPC bootstrap
|
|
if (self.rpc) {
|
|
[webView stringByEvaluatingJavaScriptFromString:[self webViewBridgeRPCBootstrap]];
|
|
}
|
|
|
|
if (_injectedJavaScript != nil) {
|
|
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
|
|
|
|
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
|
|
event[@"jsEvaluationValue"] = jsEvaluationValue;
|
|
|
|
_onLoadingFinish(event);
|
|
}
|
|
// we only need the final 'finishLoad' call so only fire the event when we're actually done loading.
|
|
else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) {
|
|
_onLoadingFinish([self baseEvent]);
|
|
}
|
|
}
|
|
|
|
- (void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
|
|
{
|
|
if (!hideKeyboardAccessoryView) {
|
|
return;
|
|
}
|
|
|
|
UIView* subview;
|
|
for (UIView* view in _webView.scrollView.subviews) {
|
|
if([[view.class description] hasPrefix:@"UIWeb"])
|
|
subview = view;
|
|
}
|
|
|
|
if(subview == nil) return;
|
|
|
|
NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelper", 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([_SwizzleHelper class], @selector(inputAccessoryView));
|
|
class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method));
|
|
|
|
objc_registerClassPair(newClass);
|
|
}
|
|
|
|
object_setClass(subview, newClass);
|
|
}
|
|
|
|
- (void)sendToBridge:(NSString *)message
|
|
{
|
|
//we are warpping the send message in a function to make sure that if
|
|
//WebView is not injected, we don't crash the app.
|
|
NSString *format = NSStringMultiline(
|
|
(function(){
|
|
if (WebViewBridge && WebViewBridge.__push__) {
|
|
WebViewBridge.__push__("%@");
|
|
}
|
|
}());
|
|
);
|
|
|
|
NSString *command = [NSString stringWithFormat: format, message];
|
|
[_webView stringByEvaluatingJavaScriptFromString:command];
|
|
}
|
|
|
|
- (NSArray*)stringArrayJsonToArray:(NSString *)message
|
|
{
|
|
return [NSJSONSerialization JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding]
|
|
options:NSJSONReadingAllowFragments
|
|
error:nil];
|
|
}
|
|
|
|
- (NSString *)webViewBridgeBootrstrap
|
|
{
|
|
NSString *path = [[NSBundle mainBundle] pathForResource:@"WebViewBridge" ofType:@"js"];
|
|
assert(path != nil);
|
|
NSString* content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
|
|
return content;
|
|
}
|
|
|
|
- (NSString *)webViewBridgeRPCBootstrap
|
|
{
|
|
NSString *path = [[NSBundle mainBundle] pathForResource:@"WebViewBridgeRPC" ofType:@"js"];
|
|
assert(path != nil);
|
|
NSString* content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];
|
|
return content;
|
|
}
|
|
|
|
@end
|