react-native/ReactKit/Executors/RCTWebViewExecutor.m

185 lines
5.5 KiB
Objective-C

// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTWebViewExecutor.h"
#import <objc/runtime.h>
#import "RCTLog.h"
#import "RCTUtils.h"
static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...)
{
va_list args;
va_start(args, fmt);
NSString *description = [[NSString alloc] initWithFormat:fmt arguments:args];
RCTLogError(@"%@", description);
NSError *error = [NSError errorWithDomain:NSStringFromClass([RCTWebViewExecutor class])
code:3
userInfo:@{NSLocalizedDescriptionKey:description}];
callback(nil, error);
va_end(args);
}
@interface RCTWebViewExecutor () <UIWebViewDelegate>
@end
@implementation RCTWebViewExecutor
{
UIWebView *_webView;
NSMutableDictionary *_objectsToInject;
}
- (instancetype)initWithWebView:(UIWebView *)webView
{
if (!webView) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Can't init with a nil webview" userInfo:nil];
}
if ((self = [super init])) {
_objectsToInject = [[NSMutableDictionary alloc] init];
_webView = webView;
_webView.delegate = self;
}
return self;
}
- (id)init
{
return [self initWithWebView:[[UIWebView alloc] init]];
}
- (BOOL)isValid
{
return _webView != nil;
}
- (void)invalidate
{
_webView.delegate = nil;
_webView = nil;
}
- (UIWebView *)invalidateAndReclaimWebView
{
UIWebView *webView = _webView;
[self invalidate];
return webView;
}
- (void)executeJSCall:(NSString *)name
method:(NSString *)method
arguments:(NSArray *)arguments
callback:(RCTJavaScriptCallback)onComplete
{
RCTAssert(onComplete != nil, @"");
[self executeBlockOnJavaScriptQueue:^{
NSError *error;
NSString *argsString = RCTJSONStringify(arguments, &error);
if (!argsString) {
RCTReportError(onComplete, @"Cannot convert argument to string: %@", error);
return;
}
NSString *execString = [NSString stringWithFormat:@"JSON.stringify(require('%@').%@.apply(null, %@));", name, method, argsString];
NSString *ret = [_webView stringByEvaluatingJavaScriptFromString:execString];
if (ret.length == 0) {
RCTReportError(onComplete, @"Empty return string: JavaScript error running script: %@", execString);
return;
}
id objcValue = RCTJSONParse(ret, &error);
if (!objcValue) {
RCTReportError(onComplete, @"Cannot parse json response: %@", error);
return;
}
onComplete(objcValue, nil);
}];
}
/**
* We cannot use the standard eval JS method. Source will not show up in the
* debugger. So we have to use this (essentially) async API - and register
* ourselves as the webview delegate to be notified when load is complete.
*/
- (void)executeApplicationScript:(NSString *)script
sourceURL:(NSURL *)url
onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self executeApplicationScript:script sourceURL:url onComplete:onComplete];
});
return;
}
RCTAssert(onComplete != nil, @"");
_onApplicationScriptLoaded = onComplete;
if (_objectsToInject.count > 0) {
NSMutableString *scriptWithInjections = [[NSMutableString alloc] initWithString:@"/* BEGIN NATIVELY INJECTED OBJECTS */\n"];
[_objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *blockScript, BOOL *stop) {
[scriptWithInjections appendString:objectName];
[scriptWithInjections appendString:@" = ("];
[scriptWithInjections appendString:blockScript];
[scriptWithInjections appendString:@");\n"];
}];
[_objectsToInject removeAllObjects];
[scriptWithInjections appendString:@"/* END NATIVELY INJECTED OBJECTS */\n"];
[scriptWithInjections appendString:script];
script = scriptWithInjections;
}
NSString *runScript =
[NSString
stringWithFormat:@"<html><head></head><body><script type='text/javascript'>%@</script></body></html>",
script
];
[_webView loadHTMLString:runScript baseURL:url];
}
/**
* In order to avoid `UIWebView` thread locks, all JS executions should be
* performed outside of the event loop that notifies the `UIWebViewDelegate`
* that the page has loaded. This is only an issue with the remote debug mode of
* `UIWebView`. For a production `UIWebView` deployment, this delay is
* unnecessary and possibly harmful (or helpful?)
*
* The delay might not be needed as soon as the following change lands into
* iOS7. (Review the patch linked here and search for "crash"
* https://bugs.webkit.org/show_bug.cgi?id=125746).
*/
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC);
dispatch_after(when, dispatch_get_main_queue(), ^{
RCTAssertMainThread();
block();
});
}
/**
* `UIWebViewDelegate` methods. Handle application script load.
*/
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
RCTAssertMainThread();
if (_onApplicationScriptLoaded) {
_onApplicationScriptLoaded(nil); // TODO(frantic): how to fetch error from UIWebView?
}
_onApplicationScriptLoaded = nil;
}
- (void)injectJSONText:(NSString *)script
asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete
{
RCTAssert(!_objectsToInject[objectName], @"already injected object named %@", _objectsToInject[objectName]);
_objectsToInject[objectName] = script;
onComplete(nil);
}
@end