From 7e9cc217603927aa2cf64e0eaeb50ddeedb4345a 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. [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. https://github.com/facebook/react-native/assets/52801365/892dcfe7-246f-4f36-be37-12c139c207ac 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 | 285 ++++++++++-------- 1 file changed, 161 insertions(+), 124 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index 736494693ebc21..f3da456345299a 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -27,7 +27,7 @@ #if RCT_DEV_MENU -@class RCTRedBoxWindow; +@class RCTRedBoxController; #if !TARGET_OS_OSX // [macOS] @interface UIButton (RCTRedBox) @@ -66,121 +66,172 @@ - (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UICo @end #endif // [macOS] -@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 #if !TARGET_OS_OSX // [macOS] -@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; + } - _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]; - - 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 @@ -231,7 +282,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 && @@ -251,19 +302,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 @@ -401,7 +452,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]; } @@ -790,7 +841,7 @@ - (NSAttributedString *)attributedStringForRow:(NSUInteger)row @interface RCTRedBox () < RCTInvalidating, - RCTRedBoxWindowActionDelegate, + RCTRedBoxControllerActionDelegate, #if !TARGET_OS_OSX // [macOS] RCTRedBoxExtraDataActionDelegate, #endif // [macOS] @@ -798,7 +849,7 @@ @interface RCTRedBox () < @end @implementation RCTRedBox { - RCTRedBoxWindow *_window; + RCTRedBoxController *_controller; NSMutableArray> *_errorCustomizers; #if !TARGET_OS_OSX // [macOS] RCTRedBoxExtraDataViewController *_extraDataViewController; @@ -950,30 +1001,18 @@ - (void)showErrorMessage:(NSString *)message [[self->_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"collectRedBoxExtraData" body:nil]; #pragma clang diagnostic pop - - if (!self->_window) { -#if !TARGET_OS_OSX // [macOS] -#if !TARGET_OS_VISION // [macOS] - self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds - customButtonTitles:self->_customButtonTitles - customButtonHandlers:self->_customButtonHandlers]; -#else // [visionOS - self->_window = [[RCTRedBoxWindow alloc] initWithFrame:CGRectMake(0, 0, 1280, 720) - customButtonTitles:self->_customButtonTitles - customButtonHandlers:self->_customButtonHandlers]; -#endif // visionOS] -#else // [macOS - self->_window = [RCTRedBoxWindow new]; -#endif // macOS] - 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]; }); } @@ -981,10 +1020,8 @@ - (void)loadExtraDataViewController { #if !TARGET_OS_OSX // [macOS] 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]; } }); #endif // [macOS] @@ -1002,8 +1039,7 @@ - (void)loadExtraDataViewController { [self->_window performSelectorOnMainThread:@selector(dismiss) withObject:nil waitUntilDone:NO]; #else // [macOS dispatch_async(dispatch_get_main_queue(), ^{ - [self->_window dismiss]; - self->_window = nil; // [macOS] release _window now to ensure its UIKit ivars are dealloc'd on the main thread as the RCTRedBox can be dealloc'd on a background thread. + [self->_controller dismiss]; }); #endif // macOS] } @@ -1013,7 +1049,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"]) { @@ -1036,10 +1073,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();