Files

403 lines
13 KiB
Objective-C
Raw Permalink Normal View History

2015-12-08 12:06:22 -05:00
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* Copyright (c) 2015-present, Ali Najafizadeh (github.com/alinz)
* 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 "RCTAutoInsetsProtocol.h"
#import "RCTConvert.h"
2015-12-08 12:06:22 -05:00
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "UIView+React.h"
2015-12-08 17:16:58 -05:00
//This is a very elegent way of defining multiline string in objective-c.
2015-12-08 17:06:59 -05:00
//source: http://stackoverflow.com/a/23387659/828487
#define NSStringMultiline(...) [[NSString alloc] initWithCString:#__VA_ARGS__ encoding:NSUTF8StringEncoding]
2015-12-08 13:26:58 -05:00
//we don'e need this one since it has been defined in RCTWebView.m
//NSString *const RCTJSNavigationScheme = @"react-js-navigation";
2015-12-08 14:27:53 -05:00
NSString *const RCTWebViewBridgeSchema = @"wvb";
2015-12-08 12:06:22 -05:00
// 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
2015-12-08 12:06:22 -05:00
@interface RCTWebViewBridge () <UIWebViewDelegate, RCTAutoInsetsProtocol>
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
2015-12-08 17:06:59 -05:00
@property (nonatomic, copy) RCTDirectEventBlock onBridgeMessage;
2015-12-08 12:06:22 -05:00
@end
@implementation RCTWebViewBridge
{
UIWebView *_webView;
NSString *_injectedJavaScript;
}
- (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];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (void)goForward
{
[_webView goForward];
}
- (void)goBack
{
[_webView goBack];
}
- (void)reload
{
[_webView reload];
}
2015-12-08 17:16:58 -05:00
- (void)sendToBridge:(NSString *)message
2015-12-08 17:06:59 -05:00
{
//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];
}
2015-12-08 12:06:22 -05:00
- (NSURL *)URL
{
return _webView.request.URL;
}
- (void)setSource:(NSDictionary *)source
2015-12-08 12:06:22 -05:00
{
if (![_source isEqualToDictionary:source]) {
_source = [source copy];
2015-12-08 12:06:22 -05:00
// Check for a static html source first
NSString *html = [RCTConvert NSString:source[@"html"]];
if (html) {
NSURL *baseURL = [RCTConvert NSURL:source[@"baseUrl"]];
[_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];
}
2015-12-08 12:06:22 -05:00
}
- (void)layoutSubviews
{
[super layoutSubviews];
_webView.frame = self.bounds;
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
_contentInset = contentInset;
[RCTView autoAdjustInsetsForView:self
withScrollView:_webView.scrollView
updateOffset:NO];
}
- (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];
}
-(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);
}
2015-12-08 12:06:22 -05:00
#pragma mark - UIWebViewDelegate methods
- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme];
2015-12-08 17:06:59 -05:00
if (!isJSNavigation && [request.URL.scheme isEqualToString:RCTWebViewBridgeSchema]) {
2015-12-09 12:20:33 -05:00
NSString* message = [webView stringByEvaluatingJavaScriptFromString:@"WebViewBridge.__fetch__()"];
2015-12-08 14:27:53 -05:00
2015-12-08 17:06:59 -05:00
NSMutableDictionary<NSString *, id> *onBridgeMessageEvent = [[NSMutableDictionary alloc] initWithDictionary:@{
@"messages": [self stringArrayJsonToArray: message]
}];
_onBridgeMessage(onBridgeMessageEvent);
2015-12-08 14:27:53 -05:00
isJSNavigation = YES;
}
2015-12-08 12:06:22 -05:00
// skip this for the JS Navigation handler
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"url": (request.URL).absoluteString,
@"navigationType": @(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": @(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
{
2015-12-08 17:16:58 -05:00
//injecting WebViewBridge Script
2015-12-09 12:09:56 -05:00
NSString *webViewBridgeScriptContent = [self webViewBridgeScript];
2015-12-08 17:16:58 -05:00
[webView stringByEvaluatingJavaScriptFromString:webViewBridgeScriptContent];
//////////////////////////////////////////////////////////////////////////////
2015-12-08 12:06:22 -05:00
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]);
}
}
2015-12-08 14:27:53 -05:00
- (NSArray*)stringArrayJsonToArray:(NSString *)message
{
return [NSJSONSerialization JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding]
options:NSJSONReadingAllowFragments
error:nil];
}
2015-12-09 12:09:56 -05:00
//since there is no easy way to load the static lib resource in ios,
//we are loading the script from this method.
- (NSString *)webViewBridgeScript {
// NSBundle *bundle = [NSBundle mainBundle];
// NSString *webViewBridgeScriptFile = [bundle pathForResource:@"webviewbridge"
// ofType:@"js"];
// NSString *webViewBridgeScriptContent = [NSString stringWithContentsOfFile:webViewBridgeScriptFile
// encoding:NSUTF8StringEncoding
// error:nil];
return NSStringMultiline(
(function (window) {
'use strict';
//Make sure that if WebViewBridge already in scope we don't override it.
if (window.WebViewBridge) {
return;
}
var RNWBSchema = 'wvb';
var sendQueue = [];
var receiveQueue = [];
var doc = window.document;
var customEvent = doc.createEvent('Event');
2015-12-09 12:09:56 -05:00
function callFunc(func, message) {
if ('function' === typeof func) {
func(message);
}
}
function signalNative() {
window.location = RNWBSchema + '://message' + new Date().getTime();
}
//I made the private function ugly signiture so user doesn't called them accidently.
//if you do, then I have nothing to say. :(
var WebViewBridge = {
//this function will be called by native side to push a new message
//to webview.
__push__: function (message) {
receiveQueue.push(message);
//reason I need this setTmeout is to return this function as fast as
//possible to release the native side thread.
setTimeout(function () {
var message = receiveQueue.pop();
callFunc(WebViewBridge.onMessage, message);
}, 15); //this magic number is just a random small value. I don't like 0.
},
__fetch__: function () {
//since our sendQueue array only contains string, and our connection to native
//can only accept string, we need to convert array of strings into single string.
var messages = JSON.stringify(sendQueue);
//we make sure that sendQueue is resets
sendQueue = [];
//return the messages back to native side.
return messages;
},
//make sure message is string. because only string can be sent to native,
//if you don't pass it as string, onError function will be called.
send: function (message) {
if ('string' !== typeof message) {
callFunc(WebViewBridge.onError, "message is type '" + typeof message + "', and it needs to be string");
return;
}
//we queue the messages to make sure that native can collects all of them in one shot.
sendQueue.push(message);
//signal the objective-c that there is a message in the queue
signalNative();
},
onMessage: null,
onError: null
};
window.WebViewBridge = WebViewBridge;
//dispatch event
customEvent.initEvent('WebViewBridge', true, true);
doc.dispatchEvent(customEvent);
2015-12-09 12:09:56 -05:00
}(window));
);
}
2015-12-08 12:06:22 -05:00
@end