Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(iOS): adjust RCTRedBox to work for iPad and support orientation changes #41217

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 83 additions & 72 deletions packages/react-native/React/CoreModules/RCTRedBox.mm
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

#if RCT_DEV_MENU

@class RCTRedBoxWindow;
@class RCTRedBoxController;

@interface UIButton (RCTRedBox)

Expand Down Expand Up @@ -62,41 +62,44 @@ - (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UICo

@end

@protocol RCTRedBoxWindowActionDelegate <NSObject>
@protocol RCTRedBoxControllerActionDelegate <NSObject>

- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
- (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromRedBoxController:(RCTRedBoxController *)redBoxController;
- (void)loadExtraDataViewController;

@end

@interface RCTRedBoxWindow : NSObject <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UIViewController *rootViewController;
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
@interface RCTRedBoxController : UIViewController <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, weak) id<RCTRedBoxControllerActionDelegate> actionDelegate;
@end

@implementation RCTRedBoxWindow {
@implementation RCTRedBoxController {
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
NSArray<NSString *> *_customButtonTitles;
NSArray<RCTRedBoxButtonPressHandler> *_customButtonHandlers;
int _lastErrorCookie;
}

- (instancetype)initWithFrame:(CGRect)frame
customButtonTitles:(NSArray<NSString *> *)customButtonTitles
customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers
{
if (self = [super init]) {
_lastErrorCookie = -1;
- (instancetype) initWithCustomButtonTitles:(NSArray<NSString *> *)customButtonTitles
customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers {
if (self = [super init]) {
_lastErrorCookie = -1;
_customButtonTitles = customButtonTitles;
_customButtonHandlers = customButtonHandlers;
}

return self;
}

_rootViewController = [UIViewController new];
UIView *rootView = _rootViewController.view;
rootView.frame = frame;
rootView.backgroundColor = [UIColor blackColor];
- (void)viewDidLoad {
self.view.backgroundColor = [UIColor blackColor];

const CGFloat buttonHeight = 60;

CGRect detailsFrame = rootView.bounds;
CGRect detailsFrame = self.view.bounds;
detailsFrame.size.height -= buttonHeight + (double)[self bottomSafeViewHeight];

_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
Expand All @@ -107,7 +110,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[rootView addSubview:_stackTraceTableView];
[self.view addSubview:_stackTraceTableView];

#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
NSString *reloadText = @"Reload\n(\u2318R)";
Expand Down Expand Up @@ -137,45 +140,56 @@ - (instancetype)initWithFrame:(CGRect)frame
accessibilityIdentifier:@"redbox-extra"
selector:@selector(showExtraDataViewController)
block:nil];

CGFloat buttonWidth = frame.size.width / (CGFloat)(4 + [customButtonTitles count]);
CGFloat bottomButtonHeight = frame.size.height - buttonHeight - (CGFloat)[self bottomSafeViewHeight];
dismissButton.frame = CGRectMake(0, bottomButtonHeight, buttonWidth, buttonHeight);
reloadButton.frame = CGRectMake(buttonWidth, bottomButtonHeight, buttonWidth, buttonHeight);
copyButton.frame = CGRectMake(buttonWidth * 2, bottomButtonHeight, buttonWidth, buttonHeight);
extraButton.frame = CGRectMake(buttonWidth * 3, bottomButtonHeight, buttonWidth, buttonHeight);

[rootView addSubview:dismissButton];
[rootView addSubview:reloadButton];
[rootView addSubview:copyButton];
[rootView addSubview:extraButton];

for (NSUInteger i = 0; i < [customButtonTitles count]; i++) {
UIButton *button = [self redBoxButton:customButtonTitles[i]

[dismissButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
[reloadButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
[copyButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
[extraButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;

UIStackView *buttonStackView = [[UIStackView alloc] init];
buttonStackView.translatesAutoresizingMaskIntoConstraints = NO;
buttonStackView.axis = UILayoutConstraintAxisHorizontal;
buttonStackView.distribution = UIStackViewDistributionFillEqually;
buttonStackView.alignment = UIStackViewAlignmentTop;

[buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight+[self bottomSafeViewHeight]].active = YES;
buttonStackView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];

[buttonStackView addArrangedSubview:dismissButton];
[buttonStackView addArrangedSubview:reloadButton];
[buttonStackView addArrangedSubview:copyButton];
[buttonStackView addArrangedSubview:extraButton];

[self.view addSubview:buttonStackView];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0]];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]];


for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) {
UIButton *button = [self redBoxButton:_customButtonTitles[i]
accessibilityIdentifier:@""
selector:nil
block:customButtonHandlers[i]];
button.frame = CGRectMake(buttonWidth * (double)(4 + i), bottomButtonHeight, buttonWidth, buttonHeight);
[rootView addSubview:button];
block:_customButtonHandlers[i]];
[button.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
[buttonStackView addArrangedSubview:button];
}

UIView *topBorder =
[[UIView alloc] initWithFrame:CGRectMake(0, bottomButtonHeight + 1, rootView.frame.size.width, 1)];
UIView *topBorder = [[UIView alloc] init];
topBorder.translatesAutoresizingMaskIntoConstraints = NO;
topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
[topBorder.heightAnchor constraintEqualToConstant:1].active = YES;

[rootView addSubview:topBorder];

UIView *bottomSafeView = [UIView new];
bottomSafeView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
bottomSafeView.frame = CGRectMake(
0,
frame.size.height - (CGFloat)[self bottomSafeViewHeight],
frame.size.width,
(CGFloat)[self bottomSafeViewHeight]);
[self.view addSubview:topBorder];

[self.view addConstraint:[NSLayoutConstraint constraintWithItem:topBorder attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]];

[rootView addSubview:bottomSafeView];
}
return self;
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:topBorder attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:topBorder attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:buttonStackView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]];
}

- (UIButton *)redBoxButton:(NSString *)title
Expand Down Expand Up @@ -226,7 +240,7 @@ - (void)showErrorMessage:(NSString *)message
// Remove ANSI color codes from the message
NSString *messageWithoutAnsi = [self stripAnsi:message];

BOOL isRootViewControllerPresented = self.rootViewController.presentingViewController != nil;
BOOL isRootViewControllerPresented = self.presentingViewController != nil;
// Show if this is a new message, or if we're updating the previous message
BOOL isNew = !isRootViewControllerPresented && !isUpdate;
BOOL isUpdateForSameMessage = !isNew &&
Expand All @@ -246,19 +260,19 @@ - (void)showErrorMessage:(NSString *)message
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[RCTKeyWindow().rootViewController presentViewController:self.rootViewController animated:YES completion:nil];
[RCTKeyWindow().rootViewController presentViewController:self animated:YES completion:nil];
}
}
}

- (void)dismiss
{
[self.rootViewController dismissViewControllerAnimated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
}

- (void)reload
{
[_actionDelegate reloadFromRedBoxWindow:self];
[_actionDelegate reloadFromRedBoxController:self];
}

- (void)showExtraDataViewController
Expand Down Expand Up @@ -396,7 +410,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
if (indexPath.section == 1) {
NSUInteger row = indexPath.row;
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
[_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame];
[_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame];
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Expand Down Expand Up @@ -438,13 +452,13 @@ - (BOOL)canBecomeFirstResponder

@interface RCTRedBox () <
RCTInvalidating,
RCTRedBoxWindowActionDelegate,
RCTRedBoxControllerActionDelegate,
RCTRedBoxExtraDataActionDelegate,
NativeRedBoxSpec>
@end

@implementation RCTRedBox {
RCTRedBoxWindow *_window;
RCTRedBoxController *_controller;
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
RCTRedBoxExtraDataViewController *_extraDataViewController;
NSMutableArray<NSString *> *_customButtonTitles;
Expand Down Expand Up @@ -592,17 +606,14 @@ - (void)showErrorMessage:(NSString *)message
[[self->_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"collectRedBoxExtraData"
body:nil];
#pragma clang diagnostic pop

if (!self->_window) {
self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds
customButtonTitles:self->_customButtonTitles
customButtonHandlers:self->_customButtonHandlers];
self->_window.actionDelegate = self;
if (!self->_controller) {
self->_controller = [[RCTRedBoxController alloc] initWithCustomButtonTitles:self->_customButtonTitles customButtonHandlers:self->_customButtonHandlers];
self->_controller.actionDelegate = self;
}

RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message stack:stack];
errorInfo = [self _customizeError:errorInfo];
[self->_window showErrorMessage:errorInfo.errorMessage
[self->_controller showErrorMessage:errorInfo.errorMessage
withStack:errorInfo.stack
isUpdate:isUpdate
errorCookie:errorCookie];
Expand All @@ -613,8 +624,8 @@ - (void)loadExtraDataViewController
{
dispatch_async(dispatch_get_main_queue(), ^{
// Make sure the CMD+E shortcut doesn't call this twice
if (self->_extraDataViewController != nil && ![self->_window.rootViewController presentedViewController]) {
[self->_window.rootViewController presentViewController:self->_extraDataViewController
if (self->_extraDataViewController != nil && ![self->_controller presentedViewController]) {
[self->_controller presentViewController:self->_extraDataViewController
animated:YES
completion:nil];
}
Expand All @@ -629,7 +640,7 @@ - (void)loadExtraDataViewController
RCT_EXPORT_METHOD(dismiss)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_window dismiss];
[self->_controller dismiss];
});
}

Expand All @@ -638,7 +649,7 @@ - (void)invalidate
[self dismiss];
}

- (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
- (void)redBoxController:(__unused RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
{
NSURL *const bundleURL = _overrideBundleURL ?: _bundleManager.bundleURL;
if (![bundleURL.scheme hasPrefix:@"http"]) {
Expand All @@ -661,10 +672,10 @@ - (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEd
- (void)reload
{
// Window is not used and can be nil
[self reloadFromRedBoxWindow:nil];
[self reloadFromRedBoxController:nil];
}

- (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow
- (void)reloadFromRedBoxController:(__unused RCTRedBoxController *)redBoxController
{
if (_overrideReloadAction) {
_overrideReloadAction();
Expand Down