Skip to content

Commit

Permalink
fix(iOS): adjust RCTRedBox to work for iPad and support orientation c…
Browse files Browse the repository at this point in the history
…hanges (#41217)

Summary:
When opening `RCTRedBox` on an iPad (and also visionOS) there was an issue with buttons width going out of screen. When changing screen orientation, RedBox wasn't recalculating view positions.

**Root cause**: Getting frame of root view to display this modal and basing all calculations on it.

**Solution**: Use Auto Layout to build UI that responds to orientation changes and device specific modal presentation.

I've also tested it with adding custom buttons to RedBox and it works properly.

## Changelog:

[IOS] [FIXED] - adjust RCTRedBox to work for iPad and support orientation changes

Pull Request resolved: #41217

Test Plan:
Launch the app without metro running and check out RedBox that's shown there. Also change screen orientation to see proper recalculation of view positions.

### Before

https://github.com/facebook/react-native/assets/52801365/892dcfe7-246f-4f36-be37-12c139c207ac

### After

https://github.com/facebook/react-native/assets/52801365/dfd0c3d8-5997-462d-97ec-dcc3de452e26

Reviewed By: GijsWeterings

Differential Revision: D50734569

Pulled By: javache

fbshipit-source-id: 51b854a47caf90ae46fcd32c4adcc64ec2ceb63f
  • Loading branch information
okwasniewski authored and facebook-github-bot committed Oct 30, 2023
1 parent 10c3292 commit e2eb26c
Showing 1 changed file with 161 additions and 113 deletions.
274 changes: 161 additions & 113 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,120 +62,171 @@ - (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
- (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;
const CGFloat buttonHeight = 60;

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

_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_stackTraceTableView.delegate = self;
_stackTraceTableView.dataSource = self;
_stackTraceTableView.backgroundColor = [UIColor clearColor];
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[rootView addSubview:_stackTraceTableView];
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_stackTraceTableView.delegate = self;
_stackTraceTableView.dataSource = self;
_stackTraceTableView.backgroundColor = [UIColor clearColor];
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
[self.view addSubview:_stackTraceTableView];

#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
NSString *reloadText = @"Reload\n(\u2318R)";
NSString *dismissText = @"Dismiss\n(ESC)";
NSString *copyText = @"Copy\n(\u2325\u2318C)";
NSString *extraText = @"Extra Info\n(\u2318E)";
NSString *reloadText = @"Reload\n(\u2318R)";
NSString *dismissText = @"Dismiss\n(ESC)";
NSString *copyText = @"Copy\n(\u2325\u2318C)";
NSString *extraText = @"Extra Info\n(\u2318E)";
#else
NSString *reloadText = @"Reload JS";
NSString *dismissText = @"Dismiss";
NSString *copyText = @"Copy";
NSString *extraText = @"Extra Info";
NSString *reloadText = @"Reload JS";
NSString *dismissText = @"Dismiss";
NSString *copyText = @"Copy";
NSString *extraText = @"Extra Info";
#endif

UIButton *dismissButton = [self redBoxButton:dismissText
accessibilityIdentifier:@"redbox-dismiss"
selector:@selector(dismiss)
block:nil];
UIButton *reloadButton = [self redBoxButton:reloadText
accessibilityIdentifier:@"redbox-reload"
selector:@selector(reload)
block:nil];
UIButton *copyButton = [self redBoxButton:copyText
accessibilityIdentifier:@"redbox-copy"
selector:@selector(copyStack)
block:nil];
UIButton *extraButton = [self redBoxButton:extraText
accessibilityIdentifier:@"redbox-extra"
selector:@selector(showExtraDataViewController)
UIButton *dismissButton = [self redBoxButton:dismissText
accessibilityIdentifier:@"redbox-dismiss"
selector:@selector(dismiss)
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]
accessibilityIdentifier:@""
selector:nil
block:customButtonHandlers[i]];
button.frame = CGRectMake(buttonWidth * (double)(4 + i), bottomButtonHeight, buttonWidth, buttonHeight);
[rootView addSubview:button];
}

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

[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]);

[rootView addSubview:bottomSafeView];
UIButton *reloadButton = [self redBoxButton:reloadText
accessibilityIdentifier:@"redbox-reload"
selector:@selector(reload)
block:nil];
UIButton *copyButton = [self redBoxButton:copyText
accessibilityIdentifier:@"redbox-copy"
selector:@selector(copyStack)
block:nil];
UIButton *extraButton = [self redBoxButton:extraText
accessibilityIdentifier:@"redbox-extra"
selector:@selector(showExtraDataViewController)
block:nil];

[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.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
[buttonStackView addArrangedSubview:button];
}
return self;

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;

[self.view addSubview:topBorder];

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

[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 +277,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 +297,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 +447,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 +489,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,31 +643,27 @@ - (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
withStack:errorInfo.stack
isUpdate:isUpdate
errorCookie:errorCookie];
[self->_controller showErrorMessage:errorInfo.errorMessage
withStack:errorInfo.stack
isUpdate:isUpdate
errorCookie:errorCookie];
});
}

- (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
animated:YES
completion:nil];
if (self->_extraDataViewController != nil && ![self->_controller presentedViewController]) {
[self->_controller presentViewController:self->_extraDataViewController animated:YES completion:nil];
}
});
}
Expand All @@ -629,7 +676,7 @@ - (void)loadExtraDataViewController
RCT_EXPORT_METHOD(dismiss)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self->_window dismiss];
[self->_controller dismiss];
});
}

Expand All @@ -638,7 +685,8 @@ - (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 +709,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

0 comments on commit e2eb26c

Please sign in to comment.