From e2eb26c951c3d2a0a4721428d00ffc02b501ecfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 30 Oct 2023 07:30:55 -0700 Subject: [PATCH] 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 --- .../React/CoreModules/RCTRedBox.mm | 274 ++++++++++-------- 1 file changed, 161 insertions(+), 113 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index dc58594a80702c..2a7a3116272ad9 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -25,7 +25,7 @@ #if RCT_DEV_MENU -@class RCTRedBoxWindow; +@class RCTRedBoxController; @interface UIButton (RCTRedBox) @@ -62,120 +62,171 @@ - (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UICo @end -@protocol RCTRedBoxWindowActionDelegate +@protocol RCTRedBoxControllerActionDelegate -- (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 -@property (nonatomic, strong) UIViewController *rootViewController; -@property (nonatomic, weak) id actionDelegate; +@interface RCTRedBoxController : UIViewController +@property (nonatomic, weak) id actionDelegate; @end -@implementation RCTRedBoxWindow { +@implementation RCTRedBoxController { UITableView *_stackTraceTableView; NSString *_lastErrorMessage; NSArray *_lastStackTrace; + NSArray *_customButtonTitles; + NSArray *_customButtonHandlers; int _lastErrorCookie; } -- (instancetype)initWithFrame:(CGRect)frame - customButtonTitles:(NSArray *)customButtonTitles - customButtonHandlers:(NSArray *)customButtonHandlers +- (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTitles + customButtonHandlers:(NSArray *)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 @@ -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 && @@ -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 @@ -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]; } @@ -438,13 +489,13 @@ - (BOOL)canBecomeFirstResponder @interface RCTRedBox () < RCTInvalidating, - RCTRedBoxWindowActionDelegate, + RCTRedBoxControllerActionDelegate, RCTRedBoxExtraDataActionDelegate, NativeRedBoxSpec> @end @implementation RCTRedBox { - RCTRedBoxWindow *_window; + RCTRedBoxController *_controller; NSMutableArray> *_errorCustomizers; RCTRedBoxExtraDataViewController *_extraDataViewController; NSMutableArray *_customButtonTitles; @@ -592,20 +643,18 @@ - (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]; }); } @@ -613,10 +662,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 - animated:YES - completion:nil]; + if (self->_extraDataViewController != nil && ![self->_controller presentedViewController]) { + [self->_controller presentViewController:self->_extraDataViewController animated:YES completion:nil]; } }); } @@ -629,7 +676,7 @@ - (void)loadExtraDataViewController RCT_EXPORT_METHOD(dismiss) { dispatch_async(dispatch_get_main_queue(), ^{ - [self->_window dismiss]; + [self->_controller dismiss]; }); } @@ -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"]) { @@ -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();