react-native/React/Modules/RCTRedBox.m

491 lines
17 KiB
Mathematica
Raw Normal View History

/**
* 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 "RCTRedBox.h"
2015-04-11 22:08:00 +00:00
#import "RCTBridge.h"
#import "RCTConvert.h"
2015-04-30 16:50:22 +00:00
#import "RCTDefines.h"
#import "RCTErrorInfo.h"
#import "RCTUtils.h"
#import "RCTJSStackFrame.h"
2015-04-30 16:50:22 +00:00
#if RCT_DEBUG
@class RCTRedBoxWindow;
@protocol RCTRedBoxWindowActionDelegate <NSObject>
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
@end
@interface RCTRedBoxWindow : UIWindow <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
@end
@implementation RCTRedBoxWindow
{
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.windowLevel = UIWindowLevelAlert + 1000;
2015-08-19 12:27:43 +00:00
self.backgroundColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1];
self.hidden = YES;
UIViewController *rootController = [UIViewController new];
self.rootViewController = rootController;
UIView *rootView = rootController.view;
rootView.backgroundColor = [UIColor clearColor];
const CGFloat buttonHeight = 60;
CGRect detailsFrame = rootView.bounds;
detailsFrame.size.height -= buttonHeight;
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_stackTraceTableView.delegate = self;
_stackTraceTableView.dataSource = self;
_stackTraceTableView.backgroundColor = [UIColor clearColor];
2015-08-19 12:27:43 +00:00
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[rootView addSubview:_stackTraceTableView];
#if TARGET_OS_SIMULATOR
NSString *reloadText = @"Reload JS (\u2318R)";
NSString *dismissText = @"Dismiss (ESC)";
NSString *copyText = @"Copy (\u2325\u2318C)";
#else
NSString *reloadText = @"Reload JS";
NSString *dismissText = @"Dismiss";
NSString *copyText = @"Copy";
#endif
UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom];
dismissButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin;
dismissButton.accessibilityIdentifier = @"redbox-dismiss";
dismissButton.titleLabel.font = [UIFont systemFontOfSize:14];
[dismissButton setTitle:dismissText forState:UIControlStateNormal];
2015-08-19 12:27:43 +00:00
[dismissButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal];
[dismissButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside];
UIButton *reloadButton = [UIButton buttonWithType:UIButtonTypeCustom];
reloadButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
reloadButton.accessibilityIdentifier = @"redbox-reload";
reloadButton.titleLabel.font = [UIFont systemFontOfSize:14];
[reloadButton setTitle:reloadText forState:UIControlStateNormal];
2015-08-19 12:27:43 +00:00
[reloadButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal];
[reloadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[reloadButton addTarget:self action:@selector(reload) forControlEvents:UIControlEventTouchUpInside];
UIButton *copyButton = [UIButton buttonWithType:UIButtonTypeCustom];
copyButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin;
copyButton.accessibilityIdentifier = @"redbox-copy";
copyButton.titleLabel.font = [UIFont systemFontOfSize:14];
[copyButton setTitle:copyText forState:UIControlStateNormal];
[copyButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal];
[copyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
[copyButton addTarget:self action:@selector(copyStack) forControlEvents:UIControlEventTouchUpInside];
CGFloat buttonWidth = self.bounds.size.width / 3;
dismissButton.frame = CGRectMake(0, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
copyButton.frame = CGRectMake(buttonWidth * 2, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight);
[rootView addSubview:dismissButton];
[rootView addSubview:reloadButton];
[rootView addSubview:copyButton];
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
2015-05-07 10:53:35 +00:00
- (void)dealloc
{
_stackTraceTableView.dataSource = nil;
_stackTraceTableView.delegate = nil;
2015-05-07 10:53:35 +00:00
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate
{
// Show if this is a new message, or if we're updating the previous message
if ((self.hidden && !isUpdate) || (!self.hidden && isUpdate && [_lastErrorMessage isEqualToString:message])) {
_lastStackTrace = stack;
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
// message is displayed using UILabel, which is unable to render text of
// unlimited length, so we truncate it
_lastErrorMessage = [message substringToIndex:MIN((NSUInteger)10000, message.length)];
2015-02-25 00:33:39 +00:00
[_stackTraceTableView reloadData];
if (self.hidden) {
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
}
[self makeKeyAndVisible];
[self becomeFirstResponder];
}
}
- (void)dismiss
{
self.hidden = YES;
[self resignFirstResponder];
[RCTSharedApplication().delegate.window makeKeyWindow];
}
- (void)reload
{
[_actionDelegate reloadFromRedBoxWindow:self];
}
- (void)copyStack
{
NSMutableString *fullStackTrace;
if (_lastErrorMessage != nil) {
fullStackTrace = [_lastErrorMessage mutableCopy];
[fullStackTrace appendString:@"\n\n"];
}
else {
fullStackTrace = [NSMutableString string];
}
for (RCTJSStackFrame *stackFrame in _lastStackTrace) {
[fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]];
if (stackFrame.file) {
[fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]];
}
}
UIPasteboard *pb = [UIPasteboard generalPasteboard];
[pb setString:fullStackTrace];
}
- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame
{
NSString *lineInfo = [NSString stringWithFormat:@"%@:%zd",
[stackFrame.file lastPathComponent],
stackFrame.lineNumber];
if (stackFrame.column != 0) {
lineInfo = [lineInfo stringByAppendingFormat:@":%zd", stackFrame.column];
}
return lineInfo;
}
#pragma mark - TableView
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView
{
return 2;
}
- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return section == 0 ? 1 : _lastStackTrace.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"];
return [self reuseCell:cell forErrorMessage:_lastErrorMessage];
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
NSUInteger index = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[index];
return [self reuseCell:cell forStackFrame:stackFrame];
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"msg-cell"];
cell.textLabel.accessibilityIdentifier = @"redbox-error";
cell.textLabel.textColor = [UIColor whiteColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:16];
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.textLabel.numberOfLines = 0;
cell.detailTextLabel.textColor = [UIColor whiteColor];
cell.backgroundColor = [UIColor clearColor];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
cell.textLabel.text = message;
return cell;
}
- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame
{
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
2015-08-19 12:27:43 +00:00
cell.textLabel.textColor = [UIColor colorWithWhite:1 alpha:0.9];
cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14];
cell.textLabel.numberOfLines = 2;
2015-08-19 12:27:43 +00:00
cell.detailTextLabel.textColor = [UIColor colorWithWhite:1 alpha:0.7];
cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11];
cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
cell.backgroundColor = [UIColor clearColor];
cell.selectedBackgroundView = [UIView new];
2015-08-19 12:27:43 +00:00
cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2];
}
cell.textLabel.text = stackFrame.methodName;
if (stackFrame.file) {
cell.detailTextLabel.text = [self formatFrameSource:stackFrame];
} else {
cell.detailTextLabel.text = @"";
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0) {
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16],
NSParagraphStyleAttributeName: paragraphStyle};
CGRect boundingRect = [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
return ceil(boundingRect.size.height) + 40;
} else {
return 50;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 1) {
NSUInteger row = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
[_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - Key commands
- (NSArray<UIKeyCommand *> *)keyCommands
{
// NOTE: We could use RCTKeyCommands for this, but since
// we control this window, we can use the standard, non-hacky
// mechanism instead
return @[
// Dismiss red box
[UIKeyCommand keyCommandWithInput:UIKeyInputEscape
modifierFlags:0
action:@selector(dismiss)],
// Reload
[UIKeyCommand keyCommandWithInput:@"r"
modifierFlags:UIKeyModifierCommand
action:@selector(reload)],
// Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from
// the simulator to the desktop pasteboard.
[UIKeyCommand keyCommandWithInput:@"c"
modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate
action:@selector(copyStack)]
];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
@end
@interface RCTRedBox () <RCTInvalidating, RCTRedBoxWindowActionDelegate>
@end
@implementation RCTRedBox
{
RCTRedBoxWindow *_window;
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
}
@synthesize bridge = _bridge;
2015-08-19 12:27:43 +00:00
RCT_EXPORT_MODULE()
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!self->_errorCustomizers) {
self->_errorCustomizers = [NSMutableArray array];
}
if (![self->_errorCustomizers containsObject:errorCustomizer]) {
[self->_errorCustomizers addObject:errorCustomizer];
}
});
}
// WARNING: Should only be called from the main thread/dispatch queue.
- (RCTErrorInfo *)_customizeError:(RCTErrorInfo *)error
{
RCTAssertMainQueue();
if (!self->_errorCustomizers) {
return error;
}
for (id<RCTErrorCustomizer> customizer in self->_errorCustomizers) {
RCTErrorInfo *newInfo = [customizer customizeErrorInfo:error];
if (newInfo) {
error = newInfo;
}
}
return error;
}
- (void)showError:(NSError *)error
{
[self showErrorMessage:error.localizedDescription withDetails:error.localizedFailureReason];
}
- (void)showErrorMessage:(NSString *)message
{
[self showErrorMessage:message withStack:nil isUpdate:NO];
}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details
{
NSString *combinedMessage = message;
if (details) {
combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details];
}
[self showErrorMessage:combinedMessage];
}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack
{
NSArray<RCTJSStackFrame *> *stack = [RCTJSStackFrame stackFramesWithLines:rawStack];
[self _showErrorMessage:message withStack:stack isUpdate:NO];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withStack:stack isUpdate:NO];
}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
{
[self showErrorMessage:message withStack:stack isUpdate:YES];
}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
{
[self _showErrorMessage:message withStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:isUpdate];
}
- (void)_showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate
{
2015-04-21 16:48:29 +00:00
dispatch_async(dispatch_get_main_queue(), ^{
if (!self->_window) {
self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self->_window.actionDelegate = self;
2015-04-21 16:48:29 +00:00
}
RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message
stack:stack];
errorInfo = [self _customizeError:errorInfo];
[self->_window showErrorMessage:errorInfo.errorMessage
withStack:errorInfo.stack
isUpdate:isUpdate];
2015-04-21 16:48:29 +00:00
});
}
RCT_EXPORT_METHOD(dismiss)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_window dismiss];
});
}
- (void)invalidate
{
[self dismiss];
}
- (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
{
if (![_bridge.bundleURL.scheme hasPrefix:@"http"]) {
RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager.");
return;
}
NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding];
NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length];
NSMutableURLRequest *request = [NSMutableURLRequest new];
request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:_bridge.bundleURL];
request.HTTPMethod = @"POST";
request.HTTPBody = stackFrameJSON;
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
}
- (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil];
}
2015-08-19 12:27:43 +00:00
@end
@implementation RCTBridge (RCTRedBox)
- (RCTRedBox *)redBox
{
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
return [self moduleForClass:[RCTRedBox class]];
}
@end
2015-04-21 16:48:29 +00:00
2015-04-30 16:50:22 +00:00
#else // Disabled
@implementation RCTRedBox
+ (NSString *)moduleName { return nil; }
- (void)registerErrorCustomizer:(id<RCTErrorCustomizer>)errorCustomizer {}
- (void)showError:(NSError *)message {}
2015-04-30 16:50:22 +00:00
- (void)showErrorMessage:(NSString *)message {}
- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {}
- (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack {}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
- (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack {}
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate {}
2015-04-30 16:50:22 +00:00
- (void)dismiss {}
@end
2015-08-19 12:27:43 +00:00
@implementation RCTBridge (RCTRedBox)
- (RCTRedBox *)redBox { return nil; }
@end
2015-04-21 16:48:29 +00:00
#endif