Improved logging and dev menu

This commit is contained in:
Nick Lockwood
2015-04-19 12:55:46 -07:00
parent 2186691812
commit 0b21df4a34
11 changed files with 306 additions and 139 deletions

View File

@@ -264,7 +264,9 @@ RCT_EXPORT_METHOD(stopAnimation:(NSNumber *)animationTag)
RCTAnimationExperimentalManager *strongSelf = weakSelf; RCTAnimationExperimentalManager *strongSelf = weakSelf;
NSNumber *reactTag = strongSelf->_animationRegistry[animationTag]; NSNumber *reactTag = strongSelf->_animationRegistry[animationTag];
if (!reactTag) return; if (!reactTag) {
return;
}
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
for (NSString *animationKey in view.layer.animationKeys) { for (NSString *animationKey in view.layer.animationKeys) {

View File

@@ -9,11 +9,50 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
@class RCTBridge; #import "RCTBridge.h"
#import "RCTBridgeModule.h"
#import "RCTInvalidating.h"
@interface RCTDevMenu : NSObject /**
* Developer menu, useful for exposing extra functionality when debugging.
*/
@interface RCTDevMenu : NSObject <RCTBridgeModule, RCTInvalidating>
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; /**
* Is the menu enabled. The menu is enabled by default in debug mode, but
* you may wish to disable it so that you can provide your own shake handler.
*/
@property (nonatomic, assign) BOOL shakeToShow;
/**
* Enables performance profiling.
*/
@property (nonatomic, assign) BOOL profilingEnabled;
/**
* Enables automatic polling for JS code changes. Only applicable when
* running the app from a server.
*/
@property (nonatomic, assign) BOOL liveReloadEnabled;
/**
* The time between checks for code changes. Defaults to 1 second.
*/
@property (nonatomic, assign) NSTimeInterval liveReloadPeriod;
/**
* Manually show the menu. This will.
*/
- (void)show; - (void)show;
@end @end
/**
* This category makes the developer menu instance available via the
* RCTBridge, which is useful for any class that needs to access the menu.
*/
@interface RCTBridge (RCTDevMenu)
@property (nonatomic, readonly) RCTDevMenu *devMenu;
@end

View File

@@ -9,12 +9,13 @@
#import "RCTDevMenu.h" #import "RCTDevMenu.h"
#import "RCTRedBox.h" #import "RCTBridge.h"
#import "RCTLog.h"
#import "RCTRootView.h" #import "RCTRootView.h"
#import "RCTSourceCode.h" #import "RCTSourceCode.h"
#import "RCTWebViewExecutor.h" #import "RCTUtils.h"
@interface RCTBridge (RCTDevMenu) @interface RCTBridge (Profiling)
@property (nonatomic, copy, readonly) NSArray *profile; @property (nonatomic, copy, readonly) NSArray *profile;
@@ -23,87 +24,206 @@
@end @end
static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
@implementation UIWindow (RCTDevMenu)
- (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (event.subtype == UIEventSubtypeMotionShake) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
}
}
@end
@interface RCTDevMenu () <UIActionSheetDelegate> @interface RCTDevMenu () <UIActionSheetDelegate>
@end @end
@implementation RCTDevMenu @implementation RCTDevMenu
{ {
BOOL _liveReload; NSTimer *_updateTimer;
__weak RCTBridge *_bridge; UIActionSheet *_actionSheet;
} }
- (instancetype)initWithBridge:(RCTBridge *)bridge @synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (void)initialize
{ {
if (self = [super init]) { // We're swizzling here because it's poor form to override methods in a category,
_bridge = bridge; // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
// no need to call the original implementation.
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
}
- (instancetype)init
{
if ((self = [super init])) {
_shakeToShow = YES;
_liveReloadPeriod = 1.0; // 1 second
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showOnShake)
name:RCTShowDevMenuNotification
object:nil];
} }
return self; return self;
} }
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)showOnShake
{
if (_shakeToShow) {
[self show];
}
}
- (void)show - (void)show
{ {
NSString *debugTitleChrome = _bridge.executorClass != Nil && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging"; if (_actionSheet) {
NSString *debugTitleSafari = _bridge.executorClass == [RCTWebViewExecutor class] ? @"Disable Safari Debugging" : @"Enable Safari Debugging"; return;
NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload"; }
NSString *debugTitleChrome = _bridge.executorClass && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging";
NSString *debugTitleSafari = _bridge.executorClass && _bridge.executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Enable Safari Debugging";
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling"; NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling";
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development"
delegate:self UIActionSheet *actionSheet =
cancelButtonTitle:@"Cancel" [[UIActionSheet alloc] initWithTitle:@"React Native: Development"
destructiveButtonTitle:nil delegate:self
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil]; cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil];
actionSheet.actionSheetStyle = UIBarStyleBlack; actionSheet.actionSheetStyle = UIBarStyleBlack;
[actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]]; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view];
} }
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{ {
if (buttonIndex == 0) { _actionSheet = nil;
[_bridge reload];
} else if (buttonIndex == 1) {
Class cls = NSClassFromString(@"RCTWebSocketExecutor");
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : nil;
[_bridge reload];
} else if (buttonIndex == 2) {
Class cls = [RCTWebViewExecutor class];
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : Nil;
[_bridge reload];
} else if (buttonIndex == 3) {
_liveReload = !_liveReload;
[self _pollAndReload];
} else if (buttonIndex == 4) {
if (_bridge.profile) {
[_bridge stopProfiling];
} else {
[_bridge startProfiling];
}
}
}
- (void)_pollAndReload switch (buttonIndex) {
{ case 0: {
if (_liveReload) {
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
NSURL *url = sourceCodeModule.scriptURL;
NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:url];
[self performSelectorInBackground:@selector(_checkForUpdates:) withObject:longPollURL];
}
}
- (void)_checkForUpdates:(NSURL *)URL
{
NSMutableURLRequest *longPollRequest = [NSMutableURLRequest requestWithURL:URL];
longPollRequest.timeoutInterval = 30;
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:longPollRequest returningResponse:&response error:nil];
dispatch_async(dispatch_get_main_queue(), ^{
if (_liveReload && response.statusCode == 205) {
[[RCTRedBox sharedInstance] dismiss];
[_bridge reload]; [_bridge reload];
break;
} }
[self _pollAndReload]; case 1: {
}); Class cls = NSClassFromString(@"RCTWebSocketExecutor");
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : nil;
[_bridge reload];
break;
}
case 2: {
Class cls = NSClassFromString(@"RCTWebViewExecutor");
_bridge.executorClass = (_bridge.executorClass != cls) ? cls : Nil;
[_bridge reload];
break;
}
case 3: {
self.liveReloadEnabled = !_liveReloadEnabled;
break;
}
case 4: {
self.profilingEnabled = !_profilingEnabled;
break;
}
default:
break;
}
}
- (void)setProfilingEnabled:(BOOL)enabled
{
if (_profilingEnabled == enabled) {
return;
}
_profilingEnabled = enabled;
if (_bridge.profile) {
[_bridge stopProfiling];
} else {
[_bridge startProfiling];
}
}
- (void)setLiveReloadEnabled:(BOOL)enabled
{
if (_liveReloadEnabled == enabled) {
return;
}
_liveReloadEnabled = enabled;
if (_liveReloadEnabled) {
_updateTimer = [NSTimer scheduledTimerWithTimeInterval:_liveReloadPeriod
target:self
selector:@selector(pollForUpdates)
userInfo:nil
repeats:YES];
} else {
[_updateTimer invalidate];
_updateTimer = nil;
}
}
- (void)setLiveReloadPeriod:(NSTimeInterval)liveReloadPeriod
{
_liveReloadPeriod = liveReloadPeriod;
if (_liveReloadEnabled) {
self.liveReloadEnabled = NO;
self.liveReloadEnabled = YES;
}
}
- (void)pollForUpdates
{
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
if (!sourceCodeModule) {
RCTLogError(@"RCTSourceCode module not found");
self.liveReloadEnabled = NO;
}
NSURL *longPollURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:longPollURL]
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
if (_liveReloadEnabled && HTTPResponse.statusCode == 205) {
[_bridge reload];
}
}];
}
- (BOOL)isValid
{
return !_liveReloadEnabled || _updateTimer != nil;
}
- (void)invalidate
{
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
[_updateTimer invalidate];
_updateTimer = nil;
}
@end
@implementation RCTBridge (RCTDevMenu)
- (RCTDevMenu *)devMenu
{
return self.modules[RCTBridgeModuleNameForClass([RCTDevMenu class])];
} }
@end @end

View File

@@ -1,4 +1,11 @@
// Copyright 2004-present Facebook. All Rights Reserved. /**
* 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 <UIKit/UIKit.h> #import <UIKit/UIKit.h>

View File

@@ -1,4 +1,11 @@
// Copyright 2004-present Facebook. All Rights Reserved. /**
* 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 "RCTJavaScriptLoader.h" #import "RCTJavaScriptLoader.h"

View File

@@ -31,8 +31,8 @@ const char *RCTLogLevels[] = {
static RCTLogFunction RCTCurrentLogFunction; static RCTLogFunction RCTCurrentLogFunction;
static RCTLogLevel RCTCurrentLogThreshold; static RCTLogLevel RCTCurrentLogThreshold;
void RCTLogSetup(void) __attribute__((constructor)); __attribute__((constructor))
void RCTLogSetup() static void RCTLogSetup()
{ {
RCTCurrentLogFunction = RCTDefaultLogFunction; RCTCurrentLogFunction = RCTDefaultLogFunction;

View File

@@ -57,12 +57,6 @@
*/ */
@property (nonatomic, strong) Class executorClass; @property (nonatomic, strong) Class executorClass;
/**
* If YES will watch for shake gestures and show development menu
* with options like "Reload", "Enable Debugging", etc.
*/
@property (nonatomic, assign) BOOL enableDevMenu;
/** /**
* The backing view controller of the root view. * The backing view controller of the root view.
*/ */

View File

@@ -13,7 +13,6 @@
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTContextExecutor.h" #import "RCTContextExecutor.h"
#import "RCTDevMenu.h"
#import "RCTEventDispatcher.h" #import "RCTEventDispatcher.h"
#import "RCTKeyCommands.h" #import "RCTKeyCommands.h"
#import "RCTLog.h" #import "RCTLog.h"
@@ -42,7 +41,6 @@
@implementation RCTRootView @implementation RCTRootView
{ {
RCTDevMenu *_devMenu;
RCTBridge *_bridge; RCTBridge *_bridge;
RCTTouchHandler *_touchHandler; RCTTouchHandler *_touchHandler;
NSString *_moduleName; NSString *_moduleName;
@@ -60,12 +58,6 @@
self.backgroundColor = [UIColor whiteColor]; self.backgroundColor = [UIColor whiteColor];
#ifdef DEBUG
_enableDevMenu = YES;
#endif
_bridge = bridge; _bridge = bridge;
_moduleName = moduleName; _moduleName = moduleName;
@@ -120,18 +112,6 @@
return YES; return YES;
} }
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake && self.enableDevMenu) {
if (!_devMenu) {
_devMenu = [[RCTDevMenu alloc] initWithBridge:_bridge];
}
[_devMenu show];
} else {
[super motionEnded:motion withEvent:event];
}
}
RCT_IMPORT_METHOD(AppRegistry, runApplication) RCT_IMPORT_METHOD(AppRegistry, runApplication)
RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer) RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer)

View File

@@ -85,7 +85,14 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object,
range:(NSRange){0, message.length} range:(NSRange){0, message.length}
withTemplate:@"[$4$5] \t$2"]; withTemplate:@"[$4$5] \t$2"];
_RCTLogFormat(RCTLogLevelInfo, NULL, -1, @"%@", message); // TODO: it would be good if log level was sent as a param, instead of this hack
RCTLogLevel level = RCTLogLevelInfo;
if ([message rangeOfString:@"error" options:NSCaseInsensitiveSearch].length) {
level = RCTLogLevelError;
} else if ([message rangeOfString:@"warning" options:NSCaseInsensitiveSearch].length) {
level = RCTLogLevelWarning;
}
_RCTLogFormat(level, NULL, -1, @"%@", message);
} }
return JSValueMakeUndefined(context); return JSValueMakeUndefined(context);
@@ -126,8 +133,6 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
+ (void)runRunLoopThread + (void)runRunLoopThread
{ {
// TODO (#5906496): Investigate exactly what this does and why
@autoreleasepool { @autoreleasepool {
// copy thread name to pthread name // copy thread name to pthread name
pthread_setname_np([[[NSThread currentThread] name] UTF8String]); pthread_setname_np([[[NSThread currentThread] name] UTF8String]);
@@ -273,11 +278,11 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
} }
- (void)executeApplicationScript:(NSString *)script - (void)executeApplicationScript:(NSString *)script
sourceURL:(NSURL *)url sourceURL:(NSURL *)sourceURL
onComplete:(RCTJavaScriptCompleteBlock)onComplete onComplete:(RCTJavaScriptCompleteBlock)onComplete
{ {
RCTAssert(url != nil, @"url should not be nil"); RCTAssert(sourceURL != nil, @"url should not be nil");
RCTAssert(onComplete != nil, @"onComplete block should not be nil");
__weak RCTContextExecutor *weakSelf = self; __weak RCTContextExecutor *weakSelf = self;
[self executeBlockOnJavaScriptQueue:^{ [self executeBlockOnJavaScriptQueue:^{
RCTContextExecutor *strongSelf = weakSelf; RCTContextExecutor *strongSelf = weakSelf;
@@ -286,17 +291,18 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
} }
JSValueRef jsError = NULL; JSValueRef jsError = NULL;
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script);
JSStringRef sourceURL = JSStringCreateWithCFString((__bridge CFStringRef)url.absoluteString); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString);
JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, sourceURL, 0, &jsError); JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError);
JSStringRelease(sourceURL); JSStringRelease(jsURL);
JSStringRelease(execJSString); JSStringRelease(execJSString);
NSError *error; if (onComplete) {
if (!result) { NSError *error;
error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError); if (!result) {
error = RCTNSErrorFromJSError(strongSelf->_context.ctx, jsError);
}
onComplete(error);
} }
onComplete(error);
}]; }];
} }
@@ -314,7 +320,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
asGlobalObjectNamed:(NSString *)objectName asGlobalObjectNamed:(NSString *)objectName
callback:(RCTJavaScriptCompleteBlock)onComplete callback:(RCTJavaScriptCompleteBlock)onComplete
{ {
RCTAssert(onComplete != nil, @"onComplete block should not be nil");
#if DEBUG #if DEBUG
RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script); RCTAssert(RCTJSONParse(script, NULL) != nil, @"%@ wasn't valid JSON!", script);
#endif #endif
@@ -333,19 +339,21 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script]; NSString *errorDesc = [NSString stringWithFormat:@"Can't make JSON value from script '%@'", script];
RCTLogError(@"%@", errorDesc); RCTLogError(@"%@", errorDesc);
NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}]; if (onComplete) {
onComplete(error); NSError *error = [NSError errorWithDomain:@"JS" code:2 userInfo:@{NSLocalizedDescriptionKey: errorDesc}];
onComplete(error);
}
return; return;
} }
JSObjectRef globalObject = JSContextGetGlobalObject(strongSelf->_context.ctx); JSObjectRef globalObject = JSContextGetGlobalObject(strongSelf->_context.ctx);
JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName); JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)objectName);
JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL); JSObjectSetProperty(strongSelf->_context.ctx, globalObject, JSName, valueToInject, kJSPropertyAttributeNone, NULL);
JSStringRelease(JSName); JSStringRelease(JSName);
onComplete(nil); if (onComplete) {
onComplete(nil);
}
}]; }];
} }
@end @end

View File

@@ -19,10 +19,6 @@
NSUInteger _reloadRetries; NSUInteger _reloadRetries;
} }
#ifndef DEBUG
static NSUInteger RCTReloadRetries = 0;
#endif
RCT_EXPORT_MODULE() RCT_EXPORT_MODULE()
- (instancetype)initWithDelegate:(id<RCTExceptionsManagerDelegate>)delegate - (instancetype)initWithDelegate:(id<RCTExceptionsManagerDelegate>)delegate
@@ -47,27 +43,32 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message
return; return;
} }
#ifdef DEBUG #if DEBUG
[[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack];
#else #else
if (RCTReloadRetries < _maxReloadAttempts) {
RCTReloadRetries++; static NSUInteger reloadRetries = 0;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; const NSUInteger maxMessageLength = 75;
if (reloadRetries < _maxReloadAttempts) {
reloadRetries++;
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil];
} else { } else {
NSError *error;
const NSUInteger MAX_SANITIZED_LENGTH = 75;
// Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values. // Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values.
NSString *pattern = @"[+-]?\\d+[,.]?\\d*"; NSString *pattern = @"[+-]?\\d+[,.]?\\d*";
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; NSString *sanitizedMessage = [message stringByReplacingOccurrencesOfString:pattern withString:@"<num>" options:NSRegularExpressionSearch range:(NSRange){0, message.length}];
RCTAssert(error == nil, @"Bad regex pattern: %@", pattern);
NSString *sanitizedMessage = [regex stringByReplacingMatchesInString:message if (sanitizedMessage.length > maxMessageLength) {
options:0 sanitizedMessage = [[sanitizedMessage substringToIndex:maxMessageLength] stringByAppendingString:@"..."];
range:NSMakeRange(0, message.length)
withTemplate:@"<num>"];
if (sanitizedMessage.length > MAX_SANITIZED_LENGTH) {
sanitizedMessage = [[sanitizedMessage substringToIndex:MAX_SANITIZED_LENGTH] stringByAppendingString:@"..."];
} }
NSMutableString *prettyStack = [@"\n" mutableCopy];
NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"];
for (NSDictionary *frame in stack) { for (NSDictionary *frame in stack) {
[prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]];
} }
@@ -75,13 +76,21 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message
NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage]; NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage];
[NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack];
} }
#endif #endif
} }
RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message
stack:(NSArray *)stack) stack:(NSArray *)stack)
{ {
#if DEBUG
[[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack];
#endif
} }
@end @end

View File

@@ -999,7 +999,7 @@ RCT_EXPORT_METHOD(measureLayoutRelativeToParent:(NSNumber *)reactTag
* Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the * Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the
* passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts. * passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts.
*/ */
RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect
parentView:(NSNumber *)reactTag parentView:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback) callback:(RCTResponseSenderBlock)callback)
@@ -1011,7 +1011,7 @@ RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect
} }
NSArray *childShadowViews = [shadowView reactSubviews]; NSArray *childShadowViews = [shadowView reactSubviews];
NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]]; NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]];
CGRect layoutRect = [RCTConvert CGRect:rect];
[childShadowViews enumerateObjectsUsingBlock:^(RCTShadowView *childShadowView, NSUInteger idx, BOOL *stop) { [childShadowViews enumerateObjectsUsingBlock:^(RCTShadowView *childShadowView, NSUInteger idx, BOOL *stop) {
CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView]; CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView];
@@ -1026,10 +1026,11 @@ RCT_EXPORT_METHOD(measureViewsInRect:(NSDictionary *)rect
CGFloat width = childLayout.size.width; CGFloat width = childLayout.size.width;
CGFloat height = childLayout.size.height; CGFloat height = childLayout.size.height;
if (leftOffset <= layoutRect.origin.x + layoutRect.size.width && if (leftOffset <= rect.origin.x + rect.size.width &&
leftOffset + width >= layoutRect.origin.x && leftOffset + width >= rect.origin.x &&
topOffset <= layoutRect.origin.y + layoutRect.size.height && topOffset <= rect.origin.y + rect.size.height &&
topOffset + height >= layoutRect.origin.y) { topOffset + height >= rect.origin.y) {
// This view is within the layout rect // This view is within the layout rect
NSDictionary *result = @{@"index": @(idx), NSDictionary *result = @{@"index": @(idx),
@"left": @(leftOffset), @"left": @(leftOffset),