mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
fix(iOS): adjust RCTRedBox to work for iPad and support orientation changes (#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: https://github.com/facebook/react-native/pull/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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
10c3292694
commit
e2eb26c951
@@ -25,7 +25,7 @@
|
||||
|
||||
#if RCT_DEV_MENU
|
||||
|
||||
@class RCTRedBoxWindow;
|
||||
@class RCTRedBoxController;
|
||||
|
||||
@interface UIButton (RCTRedBox)
|
||||
|
||||
@@ -62,120 +62,171 @@
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
_rootViewController = [UIViewController new];
|
||||
UIView *rootView = _rootViewController.view;
|
||||
rootView.frame = frame;
|
||||
rootView.backgroundColor = [UIColor blackColor];
|
||||
return self;
|
||||
}
|
||||
|
||||
const CGFloat buttonHeight = 60;
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
self.view.backgroundColor = [UIColor blackColor];
|
||||
|
||||
CGRect detailsFrame = rootView.bounds;
|
||||
detailsFrame.size.height -= buttonHeight + (double)[self bottomSafeViewHeight];
|
||||
const CGFloat buttonHeight = 60;
|
||||
|
||||
_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];
|
||||
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;
|
||||
[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];
|
||||
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];
|
||||
|
||||
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);
|
||||
[dismissButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
|
||||
[reloadButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
|
||||
[copyButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
|
||||
[extraButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
|
||||
|
||||
[rootView addSubview:dismissButton];
|
||||
[rootView addSubview:reloadButton];
|
||||
[rootView addSubview:copyButton];
|
||||
[rootView addSubview:extraButton];
|
||||
UIStackView *buttonStackView = [[UIStackView alloc] init];
|
||||
buttonStackView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
buttonStackView.axis = UILayoutConstraintAxisHorizontal;
|
||||
buttonStackView.distribution = UIStackViewDistributionFillEqually;
|
||||
buttonStackView.alignment = UIStackViewAlignmentTop;
|
||||
|
||||
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];
|
||||
}
|
||||
[buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + [self bottomSafeViewHeight]].active = YES;
|
||||
buttonStackView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
|
||||
|
||||
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];
|
||||
[buttonStackView addArrangedSubview:dismissButton];
|
||||
[buttonStackView addArrangedSubview:reloadButton];
|
||||
[buttonStackView addArrangedSubview:copyButton];
|
||||
[buttonStackView addArrangedSubview:extraButton];
|
||||
|
||||
[rootView addSubview:topBorder];
|
||||
[self.view addSubview:buttonStackView];
|
||||
|
||||
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 addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView
|
||||
attribute:NSLayoutAttributeLeading
|
||||
relatedBy:NSLayoutRelationEqual
|
||||
toItem:self.view
|
||||
attribute:NSLayoutAttributeLeading
|
||||
multiplier:1.0
|
||||
constant:0]];
|
||||
|
||||
[rootView addSubview:bottomSafeView];
|
||||
[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
|
||||
@@ -226,7 +277,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
// 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 &&
|
||||
@@ -246,19 +297,19 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
[_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
|
||||
@@ -396,7 +447,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
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];
|
||||
}
|
||||
@@ -438,13 +489,13 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
|
||||
@interface RCTRedBox () <
|
||||
RCTInvalidating,
|
||||
RCTRedBoxWindowActionDelegate,
|
||||
RCTRedBoxControllerActionDelegate,
|
||||
RCTRedBoxExtraDataActionDelegate,
|
||||
NativeRedBoxSpec>
|
||||
@end
|
||||
|
||||
@implementation RCTRedBox {
|
||||
RCTRedBoxWindow *_window;
|
||||
RCTRedBoxController *_controller;
|
||||
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
|
||||
RCTRedBoxExtraDataViewController *_extraDataViewController;
|
||||
NSMutableArray<NSString *> *_customButtonTitles;
|
||||
@@ -592,20 +643,18 @@ RCT_EXPORT_MODULE()
|
||||
[[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];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -613,10 +662,8 @@ RCT_EXPORT_MODULE()
|
||||
{
|
||||
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];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -629,7 +676,7 @@ RCT_EXPORT_METHOD(setExtraData : (NSDictionary *)extraData forIdentifier : (NSSt
|
||||
RCT_EXPORT_METHOD(dismiss)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self->_window dismiss];
|
||||
[self->_controller dismiss];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,7 +685,8 @@ RCT_EXPORT_METHOD(dismiss)
|
||||
[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"]) {
|
||||
@@ -661,10 +709,10 @@ RCT_EXPORT_METHOD(dismiss)
|
||||
- (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();
|
||||
|
||||
Reference in New Issue
Block a user