From cc0f6b54aad29e442219e57626a5a3815a944775 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Wed, 22 Apr 2026 07:15:23 -0700 Subject: [PATCH] Add LogBox-styled error overlay (#56550) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56550 Replace the legacy red-on-black RedBox design with a LogBox-inspired design for RedBox 2.0: charcoal background, salmon header bar, structured call stack, 3-button footer, full-screen view with no animated transition. Gated behind `redBoxV2IOS`. This diff is just for the low-hanging fruit - setting up the split implementation and borrowing the broad visual style of LogBox. Further up this stack we will port more functionality and improve on this baseline. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D98115368 --- packages/react-native/Package.swift | 2 +- .../React/CoreModules/RCTRedBox+Internal.h | 36 ++ .../React/CoreModules/RCTRedBox.mm | 30 +- .../RCTRedBox2Controller+Internal.h | 31 ++ .../React/CoreModules/RCTRedBox2Controller.mm | 498 ++++++++++++++++++ .../RCTRedBoxController+Internal.h | 14 +- .../CoreModules/React-CoreModules.podspec | 1 + 7 files changed, 589 insertions(+), 23 deletions(-) create mode 100644 packages/react-native/React/CoreModules/RCTRedBox+Internal.h create mode 100644 packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h create mode 100644 packages/react-native/React/CoreModules/RCTRedBox2Controller.mm diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index 4271313cf8aa..756a066a9214 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -392,7 +392,7 @@ let reactCoreModules = RNTarget( name: .reactCoreModules, path: "React/CoreModules", excludedPaths: ["PlatformStubs/RCTStatusBarManager.mm"], - dependencies: [.reactNativeDependencies, .jsi, .yoga, .reactTurboModuleCore] + dependencies: [.reactNativeDependencies, .jsi, .yoga, .reactTurboModuleCore, .reactFeatureFlags] ) /// React-runtimeCore.podspec diff --git a/packages/react-native/React/CoreModules/RCTRedBox+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox+Internal.h new file mode 100644 index 000000000000..4e68766b39ee --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTRedBox+Internal.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#if RCT_DEV_MENU + +@class RCTJSStackFrame; + +@protocol RCTRedBoxControllerActionDelegate + +- (void)redBoxController:(UIViewController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame; +- (void)reloadFromRedBoxController:(UIViewController *)redBoxController; +- (void)loadExtraDataViewController; + +@end + +@protocol RCTRedBoxControlling + +@property (nonatomic, weak) id actionDelegate; + +- (void)showErrorMessage:(NSString *)message + withStack:(NSArray *)stack + isUpdate:(BOOL)isUpdate + errorCookie:(int)errorCookie; + +- (void)dismiss; + +@end + +#endif diff --git a/packages/react-native/React/CoreModules/RCTRedBox.mm b/packages/react-native/React/CoreModules/RCTRedBox.mm index b216df078262..f0630767607b 100644 --- a/packages/react-native/React/CoreModules/RCTRedBox.mm +++ b/packages/react-native/React/CoreModules/RCTRedBox.mm @@ -16,8 +16,11 @@ #import #import #import +#import #import "CoreModulesPlugins.h" +#import "RCTRedBox+Internal.h" +#import "RCTRedBox2Controller+Internal.h" #import "RCTRedBoxController+Internal.h" #if RCT_DEV_MENU @@ -30,7 +33,7 @@ @interface RCTRedBox () < @end @implementation RCTRedBox { - RCTRedBoxController *_controller; + id _controller; NSMutableArray> *_errorCustomizers; RCTRedBoxExtraDataViewController *_extraDataViewController; NSMutableArray *_customButtonTitles; @@ -178,14 +181,20 @@ - (void)showErrorMessage:(NSString *)message [[self->_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"collectRedBoxExtraData" body:nil]; #pragma clang diagnostic pop - 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]; + + if (self->_controller == nullptr) { + if (facebook::react::ReactNativeFeatureFlags::redBoxV2IOS()) { + self->_controller = [[RCTRedBox2Controller alloc] initWithCustomButtonTitles:self->_customButtonTitles + customButtonHandlers:self->_customButtonHandlers]; + } else { + self->_controller = [[RCTRedBoxController alloc] initWithCustomButtonTitles:self->_customButtonTitles + customButtonHandlers:self->_customButtonHandlers]; + } + self->_controller.actionDelegate = self; + } [self->_controller showErrorMessage:errorInfo.errorMessage withStack:errorInfo.stack isUpdate:isUpdate @@ -196,9 +205,10 @@ - (void)showErrorMessage:(NSString *)message - (void)loadExtraDataViewController { dispatch_async(dispatch_get_main_queue(), ^{ + UIViewController *controller = static_cast(self->_controller); // Make sure the CMD+E shortcut doesn't call this twice - if (self->_extraDataViewController != nil && ![self->_controller presentedViewController]) { - [self->_controller presentViewController:self->_extraDataViewController animated:YES completion:nil]; + if (self->_extraDataViewController != nil && ([controller presentedViewController] == nullptr)) { + [controller presentViewController:self->_extraDataViewController animated:YES completion:nil]; } }); } @@ -220,7 +230,7 @@ - (void)invalidate [self dismiss]; } -- (void)redBoxController:(__unused RCTRedBoxController *)redBoxController +- (void)redBoxController:(__unused UIViewController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame { NSURL *const bundleURL = _overrideBundleURL ?: _bundleManager.bundleURL; @@ -247,7 +257,7 @@ - (void)reload [self reloadFromRedBoxController:nil]; } -- (void)reloadFromRedBoxController:(__unused RCTRedBoxController *)redBoxController +- (void)reloadFromRedBoxController:(__unused UIViewController *)redBoxController { if (_overrideReloadAction) { _overrideReloadAction(); diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h new file mode 100644 index 000000000000..0922cd95d13c --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "RCTRedBox+Internal.h" + +#if RCT_DEV_MENU + +using RCTRedBox2ButtonPressHandler = void (^)(void); + +@interface RCTRedBox2Controller : UIViewController + +@property (nonatomic, weak) id actionDelegate; + +- (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTitles + customButtonHandlers:(NSArray *)customButtonHandlers; + +- (void)showErrorMessage:(NSString *)message + withStack:(NSArray *)stack + isUpdate:(BOOL)isUpdate + errorCookie:(int)errorCookie; + +- (void)dismiss; +@end + +#endif diff --git a/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm new file mode 100644 index 000000000000..8913d98d7530 --- /dev/null +++ b/packages/react-native/React/CoreModules/RCTRedBox2Controller.mm @@ -0,0 +1,498 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTRedBox2Controller+Internal.h" + +#import +#import +#import +#import + +#if RCT_DEV_MENU + +#pragma mark - RCTRedBox2Controller + +// Color Palette (matching LogBoxStyle.js) +static UIColor *RCTRedBox2BackgroundColor() +{ + return [UIColor colorWithRed:51.0 / 255 green:51.0 / 255 blue:51.0 / 255 alpha:1.0]; +} + +static UIColor *RCTRedBox2ErrorColor() +{ + return [UIColor colorWithRed:243.0 / 255 green:83.0 / 255 blue:105.0 / 255 alpha:1.0]; +} + +static UIColor *RCTRedBox2TextColor(CGFloat opacity) +{ + return [UIColor colorWithWhite:1.0 alpha:opacity]; +} + +@implementation RCTRedBox2Controller { + UITableView *_stackTraceTableView; + UILabel *_headerTitleLabel; + UILabel *_errorCategoryLabel; + NSString *_lastErrorMessage; + NSArray *_lastStackTrace; + NSArray *_customButtonTitles; + NSArray *_customButtonHandlers; + int _lastErrorCookie; +} + +- (instancetype)initWithCustomButtonTitles:(NSArray *)customButtonTitles + customButtonHandlers:(NSArray *)customButtonHandlers +{ + self = [super init]; + if (self != nullptr) { + _lastErrorCookie = -1; + _customButtonTitles = customButtonTitles; + _customButtonHandlers = customButtonHandlers; + self.modalPresentationStyle = UIModalPresentationFullScreen; + } + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.backgroundColor = RCTRedBox2BackgroundColor(); + + // Header bar (adds itself to self.view) + UIView *headerBar = [self createHeaderBar]; + + // Footer button bar + UIView *footerBar = [self createFooterBar]; + + // Stack trace table + _stackTraceTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _stackTraceTableView.translatesAutoresizingMaskIntoConstraints = NO; + _stackTraceTableView.delegate = self; + _stackTraceTableView.dataSource = self; + _stackTraceTableView.backgroundColor = [UIColor clearColor]; + _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; + _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite; + _stackTraceTableView.bounces = NO; + [self.view addSubview:_stackTraceTableView]; + + [NSLayoutConstraint activateConstraints:@[ + [_stackTraceTableView.topAnchor constraintEqualToAnchor:headerBar.bottomAnchor], + [_stackTraceTableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [_stackTraceTableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [_stackTraceTableView.bottomAnchor constraintEqualToAnchor:footerBar.topAnchor], + ]]; +} + +#pragma mark - Header Bar + +- (UIView *)createHeaderBar +{ + UIView *headerContainer = [[UIView alloc] init]; + headerContainer.translatesAutoresizingMaskIntoConstraints = NO; + headerContainer.backgroundColor = RCTRedBox2ErrorColor(); + + _headerTitleLabel = [[UILabel alloc] init]; + _headerTitleLabel.translatesAutoresizingMaskIntoConstraints = NO; + _headerTitleLabel.textColor = [UIColor whiteColor]; + _headerTitleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + _headerTitleLabel.textAlignment = NSTextAlignmentCenter; + [headerContainer addSubview:_headerTitleLabel]; + + [self.view addSubview:headerContainer]; + + [NSLayoutConstraint activateConstraints:@[ + [headerContainer.topAnchor constraintEqualToAnchor:self.view.topAnchor], + [headerContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [headerContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + + [_headerTitleLabel.leadingAnchor constraintEqualToAnchor:headerContainer.leadingAnchor constant:12], + [_headerTitleLabel.trailingAnchor constraintEqualToAnchor:headerContainer.trailingAnchor constant:-12], + [_headerTitleLabel.bottomAnchor constraintEqualToAnchor:headerContainer.bottomAnchor constant:-12], + [_headerTitleLabel.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:12], + ]]; + + return headerContainer; +} + +#pragma mark - Footer Bar + +- (UIView *)createFooterBar +{ + const CGFloat buttonHeight = 48; + + NSString *reloadText = @"Reload"; + NSString *dismissText = @"Dismiss"; + NSString *copyText = @"Copy"; + + UIButton *dismissButton = [self footerButton:dismissText + accessibilityIdentifier:@"redbox-dismiss" + selector:@selector(dismiss)]; + UIButton *reloadButton = [self footerButton:reloadText + accessibilityIdentifier:@"redbox-reload" + selector:@selector(reload)]; + UIButton *copyButton = [self footerButton:copyText + accessibilityIdentifier:@"redbox-copy" + selector:@selector(copyStack)]; + + UIStackView *buttonStackView = [[UIStackView alloc] init]; + buttonStackView.translatesAutoresizingMaskIntoConstraints = NO; + buttonStackView.axis = UILayoutConstraintAxisHorizontal; + buttonStackView.distribution = UIStackViewDistributionFillEqually; + buttonStackView.alignment = UIStackViewAlignmentTop; + buttonStackView.backgroundColor = RCTRedBox2BackgroundColor(); + + [buttonStackView addArrangedSubview:dismissButton]; + [buttonStackView addArrangedSubview:reloadButton]; + [buttonStackView addArrangedSubview:copyButton]; + + for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) { + UIButton *button = [self footerButton:_customButtonTitles[i] + accessibilityIdentifier:@"" + handler:_customButtonHandlers[i]]; + [buttonStackView addArrangedSubview:button]; + } + + // Shadow layer above footer + buttonStackView.layer.shadowColor = [UIColor blackColor].CGColor; + buttonStackView.layer.shadowOffset = CGSizeMake(0, -2); + buttonStackView.layer.shadowRadius = 2; + buttonStackView.layer.shadowOpacity = 0.5; + + [self.view addSubview:buttonStackView]; + + CGFloat bottomInset = [self bottomSafeViewHeight]; + + [NSLayoutConstraint activateConstraints:@[ + [buttonStackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [buttonStackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [buttonStackView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + bottomInset], + ]]; + + for (UIButton *btn in buttonStackView.arrangedSubviews) { + [btn.heightAnchor constraintEqualToConstant:buttonHeight].active = YES; + } + + return buttonStackView; +} + +- (UIButton *)styledButton:(NSString *)title accessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; + button.accessibilityIdentifier = accessibilityIdentifier; + button.titleLabel.font = [UIFont systemFontOfSize:14]; + button.titleLabel.textAlignment = NSTextAlignmentCenter; + button.backgroundColor = RCTRedBox2BackgroundColor(); + [button setTitle:title forState:UIControlStateNormal]; + [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [button setTitleColor:RCTRedBox2TextColor(0.5) forState:UIControlStateHighlighted]; + return button; +} + +- (UIButton *)footerButton:(NSString *)title + accessibilityIdentifier:(NSString *)accessibilityIdentifier + selector:(SEL)selector +{ + UIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; + [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (UIButton *)footerButton:(NSString *)title + accessibilityIdentifier:(NSString *)accessibilityIdentifier + handler:(RCTRedBox2ButtonPressHandler)handler +{ + UIButton *button = [self styledButton:title accessibilityIdentifier:accessibilityIdentifier]; + [button addAction:[UIAction actionWithHandler:^(__unused UIAction *action) { + handler(); + }] + forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (CGFloat)bottomSafeViewHeight +{ +#if TARGET_OS_MACCATALYST + return 0; +#else + return RCTKeyWindow().safeAreaInsets.bottom; +#endif +} + +#pragma mark - Error Display + +- (NSString *)stripAnsi:(NSString *)text +{ + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\x1b\\[[0-9;]*m" + options:NSRegularExpressionCaseInsensitive + error:&error]; + return [regex stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, [text length]) withTemplate:@""]; +} + +- (void)showErrorMessage:(NSString *)message + withStack:(NSArray *)stack + isUpdate:(BOOL)isUpdate + errorCookie:(int)errorCookie +{ + // Remove ANSI color codes from the message + NSString *messageWithoutAnsi = [self stripAnsi:message]; + + 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 && + (isRootViewControllerPresented && isUpdate && + ((errorCookie == -1 && [_lastErrorMessage isEqualToString:messageWithoutAnsi]) || + (errorCookie == _lastErrorCookie))); + if (isNew || isUpdateForSameMessage) { + _lastStackTrace = stack; + // message is displayed using UILabel, which is unable to render text of + // unlimited length, so we truncate it + _lastErrorMessage = [messageWithoutAnsi substringToIndex:MIN((NSUInteger)10000, messageWithoutAnsi.length)]; + _lastErrorCookie = errorCookie; + + [_stackTraceTableView reloadData]; + + if (!isRootViewControllerPresented) { + [RCTKeyWindow().rootViewController presentViewController:self animated:NO completion:nil]; + } + } +} + +- (void)dismiss +{ + [self dismissViewControllerAnimated:NO completion:nil]; +} + +- (void)reload +{ + if (_actionDelegate != nil) { + [_actionDelegate reloadFromRedBoxController:self]; + } else { + // In bridgeless mode `RCTRedBox` gets deallocated, we need to notify listeners anyway. + RCTTriggerReloadCommandListeners(@"Redbox"); + [self dismiss]; + } +} + +- (void)copyStack +{ + NSMutableString *fullStackTrace; + + if (_lastErrorMessage != nil) { + fullStackTrace = [_lastErrorMessage mutableCopy]; + [fullStackTrace appendString:@"\n\n"]; + } else { + fullStackTrace = [NSMutableString string]; + } + + for (RCTJSStackFrame *stackFrame in _lastStackTrace) { + [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]]; + if (stackFrame.file != nullptr) { + [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]]; + } + } +#if !TARGET_OS_TV + UIPasteboard *pb = [UIPasteboard generalPasteboard]; + [pb setString:fullStackTrace]; +#endif +} + +- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame +{ + NSString *fileName = RCTNilIfNull(stackFrame.file) ? [stackFrame.file lastPathComponent] : @""; + NSString *lineInfo = [NSString stringWithFormat:@"%@:%lld", fileName, (long long)stackFrame.lineNumber]; + + if (stackFrame.column != 0) { + lineInfo = [lineInfo stringByAppendingFormat:@":%lld", (long long)stackFrame.column]; + } + return lineInfo; +} + +#pragma mark - TableView DataSource & Delegate + +- (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView +{ + return _lastStackTrace.count > 0 ? 2 : 1; +} + +- (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return section == 0 ? 1 : static_cast(_lastStackTrace.count); +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 0) { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; + return [self reuseCell:cell forErrorMessage:_lastErrorMessage]; + } + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + NSUInteger index = indexPath.row; + RCTJSStackFrame *stackFrame = _lastStackTrace[index]; + return [self reuseCell:cell forStackFrame:stackFrame]; +} + +- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString *)message +{ + if (cell == nullptr) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"msg-cell"]; + cell.backgroundColor = RCTRedBox2BackgroundColor(); + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + // Error category label (e.g. "Syntax Error", "Uncaught Error") + _errorCategoryLabel = [[UILabel alloc] init]; + _errorCategoryLabel.translatesAutoresizingMaskIntoConstraints = NO; + _errorCategoryLabel.textColor = RCTRedBox2ErrorColor(); + _errorCategoryLabel.font = [UIFont systemFontOfSize:21 weight:UIFontWeightBold]; + _errorCategoryLabel.numberOfLines = 1; + [cell.contentView addSubview:_errorCategoryLabel]; + + // Error message label + UILabel *messageLabel = [[UILabel alloc] init]; + messageLabel.translatesAutoresizingMaskIntoConstraints = NO; + messageLabel.accessibilityIdentifier = @"redbox-error"; + messageLabel.textColor = [UIColor whiteColor]; + messageLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; + messageLabel.lineBreakMode = NSLineBreakByWordWrapping; + messageLabel.numberOfLines = 0; + messageLabel.tag = 100; + [cell.contentView addSubview:messageLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [_errorCategoryLabel.topAnchor constraintEqualToAnchor:cell.contentView.topAnchor constant:15], + [_errorCategoryLabel.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:12], + [_errorCategoryLabel.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-12], + + [messageLabel.topAnchor constraintEqualToAnchor:_errorCategoryLabel.bottomAnchor constant:10], + [messageLabel.leadingAnchor constraintEqualToAnchor:cell.contentView.leadingAnchor constant:12], + [messageLabel.trailingAnchor constraintEqualToAnchor:cell.contentView.trailingAnchor constant:-12], + [messageLabel.bottomAnchor constraintEqualToAnchor:cell.contentView.bottomAnchor constant:-15], + ]]; + } + + _errorCategoryLabel.text = @"Error"; + UILabel *messageLabel = [cell.contentView viewWithTag:100]; + messageLabel.text = message; + + return cell; +} + +- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame +{ + if (cell == nullptr) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; + cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14]; + cell.textLabel.lineBreakMode = NSLineBreakByCharWrapping; + cell.textLabel.numberOfLines = 2; + cell.detailTextLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; + cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; + cell.backgroundColor = [UIColor clearColor]; + cell.selectedBackgroundView = [UIView new]; + cell.selectedBackgroundView.backgroundColor = RCTRedBox2BackgroundColor(); + cell.selectedBackgroundView.layer.cornerRadius = 5; + } + + cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)"; + if (stackFrame.file != nullptr) { + cell.detailTextLabel.text = [self formatFrameSource:stackFrame]; + } else { + cell.detailTextLabel.text = @""; + } + + if (stackFrame.collapse) { + cell.textLabel.textColor = RCTRedBox2TextColor(0.4); + cell.detailTextLabel.textColor = RCTRedBox2TextColor(0.3); + } else { + cell.textLabel.textColor = [UIColor whiteColor]; + cell.detailTextLabel.textColor = RCTRedBox2TextColor(0.8); + } + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 0) { + return UITableViewAutomaticDimension; + } else { + return 50; + } +} + +- (CGFloat)tableView:(__unused UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 0) { + return 100; + } + return 50; +} + +- (UIView *)tableView:(__unused UITableView *)tableView viewForHeaderInSection:(NSInteger)section +{ + if (section == 1) { + UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 38)]; + headerView.backgroundColor = [UIColor clearColor]; + + UILabel *label = [[UILabel alloc] init]; + label.translatesAutoresizingMaskIntoConstraints = NO; + label.text = @"Call Stack"; + label.textColor = [UIColor whiteColor]; + label.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; + [headerView addSubview:label]; + + [NSLayoutConstraint activateConstraints:@[ + [label.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:12], + [label.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:-12], + [label.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:-10], + ]]; + + return headerView; + } + return nil; +} + +- (CGFloat)tableView:(__unused UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return section == 1 ? 38 : 0; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.section == 1) { + NSUInteger row = indexPath.row; + RCTJSStackFrame *stackFrame = _lastStackTrace[row]; + [_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame]; + } + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +#pragma mark - Key Commands + +- (NSArray *)keyCommands +{ + return @[ + // Dismiss red box + [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismiss)], + // Reload + [UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reload)], + // Copy = Cmd-Option C since Cmd-C in the simulator copies the pasteboard from + // the simulator to the desktop pasteboard. + [UIKeyCommand keyCommandWithInput:@"c" + modifierFlags:UIKeyModifierCommand | UIKeyModifierAlternate + action:@selector(copyStack)], + ]; +} + +- (BOOL)canBecomeFirstResponder +{ + return YES; +} + +@end + +#endif diff --git a/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h b/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h index c8e333c49dea..191f82f461f8 100644 --- a/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h +++ b/packages/react-native/React/CoreModules/RCTRedBoxController+Internal.h @@ -7,22 +7,12 @@ #import +#import "RCTRedBox+Internal.h" #import "RCTRedBox.h" #if RCT_DEV_MENU -@class RCTJSStackFrame; -@class RCTRedBoxController; - -@protocol RCTRedBoxControllerActionDelegate - -- (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame; -- (void)reloadFromRedBoxController:(RCTRedBoxController *)redBoxController; -- (void)loadExtraDataViewController; - -@end - -@interface RCTRedBoxController : UIViewController +@interface RCTRedBoxController : UIViewController @property (nonatomic, weak) id actionDelegate; diff --git a/packages/react-native/React/CoreModules/React-CoreModules.podspec b/packages/react-native/React/CoreModules/React-CoreModules.podspec index e9e095590e6e..855114667c17 100644 --- a/packages/react-native/React/CoreModules/React-CoreModules.podspec +++ b/packages/react-native/React/CoreModules/React-CoreModules.podspec @@ -51,6 +51,7 @@ Pod::Spec.new do |s| s.dependency "React-Core/CoreModulesHeaders", version s.dependency "React-RCTImage", version s.dependency "React-jsi", version + s.dependency "React-featureflags" s.dependency 'React-RCTBlob' add_dependency(s, "React-debug") add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])