Skip to content

Commit bbfae56

Browse files
committed
fix(iOS): adjust RCTRedBox to work for iPad and support orientation changes (facebook#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: facebook#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
1 parent a43d538 commit bbfae56

File tree

1 file changed

+161
-113
lines changed

1 file changed

+161
-113
lines changed

packages/react-native/React/CoreModules/RCTRedBox.mm

Lines changed: 161 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
#if RCT_DEV_MENU
2727

28-
@class RCTRedBoxWindow;
28+
@class RCTRedBoxController;
2929

3030
@interface UIButton (RCTRedBox)
3131

@@ -62,120 +62,171 @@ - (void)rct_addBlock:(RCTRedBoxButtonPressHandler)handler forControlEvents:(UICo
6262

6363
@end
6464

65-
@protocol RCTRedBoxWindowActionDelegate <NSObject>
65+
@protocol RCTRedBoxControllerActionDelegate <NSObject>
6666

67-
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
68-
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;
67+
- (void)redBoxController:(RCTRedBoxController *)redBoxController openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
68+
- (void)reloadFromRedBoxController:(RCTRedBoxController *)redBoxController;
6969
- (void)loadExtraDataViewController;
7070

7171
@end
7272

73-
@interface RCTRedBoxWindow : NSObject <UITableViewDelegate, UITableViewDataSource>
74-
@property (nonatomic, strong) UIViewController *rootViewController;
75-
@property (nonatomic, weak) id<RCTRedBoxWindowActionDelegate> actionDelegate;
73+
@interface RCTRedBoxController : UIViewController <UITableViewDelegate, UITableViewDataSource>
74+
@property (nonatomic, weak) id<RCTRedBoxControllerActionDelegate> actionDelegate;
7675
@end
7776

78-
@implementation RCTRedBoxWindow {
77+
@implementation RCTRedBoxController {
7978
UITableView *_stackTraceTableView;
8079
NSString *_lastErrorMessage;
8180
NSArray<RCTJSStackFrame *> *_lastStackTrace;
81+
NSArray<NSString *> *_customButtonTitles;
82+
NSArray<RCTRedBoxButtonPressHandler> *_customButtonHandlers;
8283
int _lastErrorCookie;
8384
}
8485

85-
- (instancetype)initWithFrame:(CGRect)frame
86-
customButtonTitles:(NSArray<NSString *> *)customButtonTitles
87-
customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers
86+
- (instancetype)initWithCustomButtonTitles:(NSArray<NSString *> *)customButtonTitles
87+
customButtonHandlers:(NSArray<RCTRedBoxButtonPressHandler> *)customButtonHandlers
8888
{
8989
if (self = [super init]) {
9090
_lastErrorCookie = -1;
91+
_customButtonTitles = customButtonTitles;
92+
_customButtonHandlers = customButtonHandlers;
93+
}
94+
95+
return self;
96+
}
9197

92-
_rootViewController = [UIViewController new];
93-
UIView *rootView = _rootViewController.view;
94-
rootView.frame = frame;
95-
rootView.backgroundColor = [UIColor blackColor];
98+
- (void)viewDidLoad
99+
{
100+
self.view.backgroundColor = [UIColor blackColor];
96101

97-
const CGFloat buttonHeight = 60;
102+
const CGFloat buttonHeight = 60;
98103

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

102-
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
103-
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
104-
_stackTraceTableView.delegate = self;
105-
_stackTraceTableView.dataSource = self;
106-
_stackTraceTableView.backgroundColor = [UIColor clearColor];
107-
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
108-
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
109-
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
110-
[rootView addSubview:_stackTraceTableView];
107+
_stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain];
108+
_stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
109+
_stackTraceTableView.delegate = self;
110+
_stackTraceTableView.dataSource = self;
111+
_stackTraceTableView.backgroundColor = [UIColor clearColor];
112+
_stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3];
113+
_stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
114+
_stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
115+
[self.view addSubview:_stackTraceTableView];
111116

112117
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
113-
NSString *reloadText = @"Reload\n(\u2318R)";
114-
NSString *dismissText = @"Dismiss\n(ESC)";
115-
NSString *copyText = @"Copy\n(\u2325\u2318C)";
116-
NSString *extraText = @"Extra Info\n(\u2318E)";
118+
NSString *reloadText = @"Reload\n(\u2318R)";
119+
NSString *dismissText = @"Dismiss\n(ESC)";
120+
NSString *copyText = @"Copy\n(\u2325\u2318C)";
121+
NSString *extraText = @"Extra Info\n(\u2318E)";
117122
#else
118-
NSString *reloadText = @"Reload JS";
119-
NSString *dismissText = @"Dismiss";
120-
NSString *copyText = @"Copy";
121-
NSString *extraText = @"Extra Info";
123+
NSString *reloadText = @"Reload JS";
124+
NSString *dismissText = @"Dismiss";
125+
NSString *copyText = @"Copy";
126+
NSString *extraText = @"Extra Info";
122127
#endif
123128

124-
UIButton *dismissButton = [self redBoxButton:dismissText
125-
accessibilityIdentifier:@"redbox-dismiss"
126-
selector:@selector(dismiss)
127-
block:nil];
128-
UIButton *reloadButton = [self redBoxButton:reloadText
129-
accessibilityIdentifier:@"redbox-reload"
130-
selector:@selector(reload)
131-
block:nil];
132-
UIButton *copyButton = [self redBoxButton:copyText
133-
accessibilityIdentifier:@"redbox-copy"
134-
selector:@selector(copyStack)
135-
block:nil];
136-
UIButton *extraButton = [self redBoxButton:extraText
137-
accessibilityIdentifier:@"redbox-extra"
138-
selector:@selector(showExtraDataViewController)
129+
UIButton *dismissButton = [self redBoxButton:dismissText
130+
accessibilityIdentifier:@"redbox-dismiss"
131+
selector:@selector(dismiss)
139132
block:nil];
140-
141-
CGFloat buttonWidth = frame.size.width / (CGFloat)(4 + [customButtonTitles count]);
142-
CGFloat bottomButtonHeight = frame.size.height - buttonHeight - (CGFloat)[self bottomSafeViewHeight];
143-
dismissButton.frame = CGRectMake(0, bottomButtonHeight, buttonWidth, buttonHeight);
144-
reloadButton.frame = CGRectMake(buttonWidth, bottomButtonHeight, buttonWidth, buttonHeight);
145-
copyButton.frame = CGRectMake(buttonWidth * 2, bottomButtonHeight, buttonWidth, buttonHeight);
146-
extraButton.frame = CGRectMake(buttonWidth * 3, bottomButtonHeight, buttonWidth, buttonHeight);
147-
148-
[rootView addSubview:dismissButton];
149-
[rootView addSubview:reloadButton];
150-
[rootView addSubview:copyButton];
151-
[rootView addSubview:extraButton];
152-
153-
for (NSUInteger i = 0; i < [customButtonTitles count]; i++) {
154-
UIButton *button = [self redBoxButton:customButtonTitles[i]
155-
accessibilityIdentifier:@""
156-
selector:nil
157-
block:customButtonHandlers[i]];
158-
button.frame = CGRectMake(buttonWidth * (double)(4 + i), bottomButtonHeight, buttonWidth, buttonHeight);
159-
[rootView addSubview:button];
160-
}
161-
162-
UIView *topBorder =
163-
[[UIView alloc] initWithFrame:CGRectMake(0, bottomButtonHeight + 1, rootView.frame.size.width, 1)];
164-
topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
165-
166-
[rootView addSubview:topBorder];
167-
168-
UIView *bottomSafeView = [UIView new];
169-
bottomSafeView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
170-
bottomSafeView.frame = CGRectMake(
171-
0,
172-
frame.size.height - (CGFloat)[self bottomSafeViewHeight],
173-
frame.size.width,
174-
(CGFloat)[self bottomSafeViewHeight]);
175-
176-
[rootView addSubview:bottomSafeView];
133+
UIButton *reloadButton = [self redBoxButton:reloadText
134+
accessibilityIdentifier:@"redbox-reload"
135+
selector:@selector(reload)
136+
block:nil];
137+
UIButton *copyButton = [self redBoxButton:copyText
138+
accessibilityIdentifier:@"redbox-copy"
139+
selector:@selector(copyStack)
140+
block:nil];
141+
UIButton *extraButton = [self redBoxButton:extraText
142+
accessibilityIdentifier:@"redbox-extra"
143+
selector:@selector(showExtraDataViewController)
144+
block:nil];
145+
146+
[dismissButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
147+
[reloadButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
148+
[copyButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
149+
[extraButton.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
150+
151+
UIStackView *buttonStackView = [[UIStackView alloc] init];
152+
buttonStackView.translatesAutoresizingMaskIntoConstraints = NO;
153+
buttonStackView.axis = UILayoutConstraintAxisHorizontal;
154+
buttonStackView.distribution = UIStackViewDistributionFillEqually;
155+
buttonStackView.alignment = UIStackViewAlignmentTop;
156+
157+
[buttonStackView.heightAnchor constraintEqualToConstant:buttonHeight + [self bottomSafeViewHeight]].active = YES;
158+
buttonStackView.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:1];
159+
160+
[buttonStackView addArrangedSubview:dismissButton];
161+
[buttonStackView addArrangedSubview:reloadButton];
162+
[buttonStackView addArrangedSubview:copyButton];
163+
[buttonStackView addArrangedSubview:extraButton];
164+
165+
[self.view addSubview:buttonStackView];
166+
167+
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView
168+
attribute:NSLayoutAttributeLeading
169+
relatedBy:NSLayoutRelationEqual
170+
toItem:self.view
171+
attribute:NSLayoutAttributeLeading
172+
multiplier:1.0
173+
constant:0]];
174+
175+
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView
176+
attribute:NSLayoutAttributeTrailing
177+
relatedBy:NSLayoutRelationEqual
178+
toItem:self.view
179+
attribute:NSLayoutAttributeTrailing
180+
multiplier:1.0
181+
constant:0]];
182+
183+
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:buttonStackView
184+
attribute:NSLayoutAttributeBottom
185+
relatedBy:NSLayoutRelationEqual
186+
toItem:self.view
187+
attribute:NSLayoutAttributeBottom
188+
multiplier:1.0
189+
constant:0]];
190+
191+
for (NSUInteger i = 0; i < [_customButtonTitles count]; i++) {
192+
UIButton *button = [self redBoxButton:_customButtonTitles[i]
193+
accessibilityIdentifier:@""
194+
selector:nil
195+
block:_customButtonHandlers[i]];
196+
[button.heightAnchor constraintEqualToConstant:buttonHeight].active = YES;
197+
[buttonStackView addArrangedSubview:button];
177198
}
178-
return self;
199+
200+
UIView *topBorder = [[UIView alloc] init];
201+
topBorder.translatesAutoresizingMaskIntoConstraints = NO;
202+
topBorder.backgroundColor = [UIColor colorWithRed:0.70 green:0.70 blue:0.70 alpha:1.0];
203+
[topBorder.heightAnchor constraintEqualToConstant:1].active = YES;
204+
205+
[self.view addSubview:topBorder];
206+
207+
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:topBorder
208+
attribute:NSLayoutAttributeLeading
209+
relatedBy:NSLayoutRelationEqual
210+
toItem:self.view
211+
attribute:NSLayoutAttributeLeading
212+
multiplier:1.0
213+
constant:0]];
214+
215+
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:topBorder
216+
attribute:NSLayoutAttributeTrailing
217+
relatedBy:NSLayoutRelationEqual
218+
toItem:self.view
219+
attribute:NSLayoutAttributeTrailing
220+
multiplier:1.0
221+
constant:0]];
222+
223+
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:topBorder
224+
attribute:NSLayoutAttributeBottom
225+
relatedBy:NSLayoutRelationEqual
226+
toItem:buttonStackView
227+
attribute:NSLayoutAttributeTop
228+
multiplier:1.0
229+
constant:0]];
179230
}
180231

181232
- (UIButton *)redBoxButton:(NSString *)title
@@ -226,7 +277,7 @@ - (void)showErrorMessage:(NSString *)message
226277
// Remove ANSI color codes from the message
227278
NSString *messageWithoutAnsi = [self stripAnsi:message];
228279

229-
BOOL isRootViewControllerPresented = self.rootViewController.presentingViewController != nil;
280+
BOOL isRootViewControllerPresented = self.presentingViewController != nil;
230281
// Show if this is a new message, or if we're updating the previous message
231282
BOOL isNew = !isRootViewControllerPresented && !isUpdate;
232283
BOOL isUpdateForSameMessage = !isNew &&
@@ -246,19 +297,19 @@ - (void)showErrorMessage:(NSString *)message
246297
[_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
247298
atScrollPosition:UITableViewScrollPositionTop
248299
animated:NO];
249-
[RCTKeyWindow().rootViewController presentViewController:self.rootViewController animated:YES completion:nil];
300+
[RCTKeyWindow().rootViewController presentViewController:self animated:YES completion:nil];
250301
}
251302
}
252303
}
253304

254305
- (void)dismiss
255306
{
256-
[self.rootViewController dismissViewControllerAnimated:YES completion:nil];
307+
[self dismissViewControllerAnimated:YES completion:nil];
257308
}
258309

259310
- (void)reload
260311
{
261-
[_actionDelegate reloadFromRedBoxWindow:self];
312+
[_actionDelegate reloadFromRedBoxController:self];
262313
}
263314

264315
- (void)showExtraDataViewController
@@ -396,7 +447,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
396447
if (indexPath.section == 1) {
397448
NSUInteger row = indexPath.row;
398449
RCTJSStackFrame *stackFrame = _lastStackTrace[row];
399-
[_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame];
450+
[_actionDelegate redBoxController:self openStackFrameInEditor:stackFrame];
400451
}
401452
[tableView deselectRowAtIndexPath:indexPath animated:YES];
402453
}
@@ -438,13 +489,13 @@ - (BOOL)canBecomeFirstResponder
438489

439490
@interface RCTRedBox () <
440491
RCTInvalidating,
441-
RCTRedBoxWindowActionDelegate,
492+
RCTRedBoxControllerActionDelegate,
442493
RCTRedBoxExtraDataActionDelegate,
443494
NativeRedBoxSpec>
444495
@end
445496

446497
@implementation RCTRedBox {
447-
RCTRedBoxWindow *_window;
498+
RCTRedBoxController *_controller;
448499
NSMutableArray<id<RCTErrorCustomizer>> *_errorCustomizers;
449500
RCTRedBoxExtraDataViewController *_extraDataViewController;
450501
NSMutableArray<NSString *> *_customButtonTitles;
@@ -592,31 +643,27 @@ - (void)showErrorMessage:(NSString *)message
592643
[[self->_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"collectRedBoxExtraData"
593644
body:nil];
594645
#pragma clang diagnostic pop
595-
596-
if (!self->_window) {
597-
self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds
598-
customButtonTitles:self->_customButtonTitles
599-
customButtonHandlers:self->_customButtonHandlers];
600-
self->_window.actionDelegate = self;
646+
if (!self->_controller) {
647+
self->_controller = [[RCTRedBoxController alloc] initWithCustomButtonTitles:self->_customButtonTitles
648+
customButtonHandlers:self->_customButtonHandlers];
649+
self->_controller.actionDelegate = self;
601650
}
602651

603652
RCTErrorInfo *errorInfo = [[RCTErrorInfo alloc] initWithErrorMessage:message stack:stack];
604653
errorInfo = [self _customizeError:errorInfo];
605-
[self->_window showErrorMessage:errorInfo.errorMessage
606-
withStack:errorInfo.stack
607-
isUpdate:isUpdate
608-
errorCookie:errorCookie];
654+
[self->_controller showErrorMessage:errorInfo.errorMessage
655+
withStack:errorInfo.stack
656+
isUpdate:isUpdate
657+
errorCookie:errorCookie];
609658
});
610659
}
611660

612661
- (void)loadExtraDataViewController
613662
{
614663
dispatch_async(dispatch_get_main_queue(), ^{
615664
// Make sure the CMD+E shortcut doesn't call this twice
616-
if (self->_extraDataViewController != nil && ![self->_window.rootViewController presentedViewController]) {
617-
[self->_window.rootViewController presentViewController:self->_extraDataViewController
618-
animated:YES
619-
completion:nil];
665+
if (self->_extraDataViewController != nil && ![self->_controller presentedViewController]) {
666+
[self->_controller presentViewController:self->_extraDataViewController animated:YES completion:nil];
620667
}
621668
});
622669
}
@@ -629,7 +676,7 @@ - (void)loadExtraDataViewController
629676
RCT_EXPORT_METHOD(dismiss)
630677
{
631678
dispatch_async(dispatch_get_main_queue(), ^{
632-
[self->_window dismiss];
679+
[self->_controller dismiss];
633680
});
634681
}
635682

@@ -638,7 +685,8 @@ - (void)invalidate
638685
[self dismiss];
639686
}
640687

641-
- (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
688+
- (void)redBoxController:(__unused RCTRedBoxController *)redBoxController
689+
openStackFrameInEditor:(RCTJSStackFrame *)stackFrame
642690
{
643691
NSURL *const bundleURL = _overrideBundleURL ?: _bundleManager.bundleURL;
644692
if (![bundleURL.scheme hasPrefix:@"http"]) {
@@ -661,10 +709,10 @@ - (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEd
661709
- (void)reload
662710
{
663711
// Window is not used and can be nil
664-
[self reloadFromRedBoxWindow:nil];
712+
[self reloadFromRedBoxController:nil];
665713
}
666714

667-
- (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow
715+
- (void)reloadFromRedBoxController:(__unused RCTRedBoxController *)redBoxController
668716
{
669717
if (_overrideReloadAction) {
670718
_overrideReloadAction();

0 commit comments

Comments
 (0)