From 9763e6c635eba224191e2bac543ae0787fecd209 Mon Sep 17 00:00:00 2001 From: Emily Brown Date: Wed, 26 Nov 2025 03:47:11 -0800 Subject: [PATCH 1/2] Fix RCTDevLoadingView crash when adding button constraints (#54690) Summary: Changelog: [iOS][Fixed] - Fixed crash from dismiss button in DevLoadingView D86420230 added a dismiss button feature to RCTDevLoadingView. However, when adding layout constraints on line 198, it incorrectly checked the `dismissButton` parameter instead of the `self->_dismissButton` instance variable. This caused NSLayoutConstraint crashes when the parameter was nil/false but the instance variable existed from a previous call, or vice versa. This changes line 198 to check `self->_dismissButton` (the instance variable) instead of `dismissButton` (the function parameter) to properly verify if the button exists before adding constraints to it. Reviewed By: vzaidman, javache Differential Revision: D87871856 --- packages/react-native/React/CoreModules/RCTDevLoadingView.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 103f7f52b7f1..07fe5e62ddc6 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -195,7 +195,7 @@ - (void)showMessage:(NSString *)message ]]; // Add button-specific constraints if button exists - if (dismissButton) { + if (self->_dismissButton != nullptr) { [constraints addObjectsFromArray:@[ [self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10], [self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor], From 37cae536dafa8550d95a1ca396e2e5296d2e24f2 Mon Sep 17 00:00:00 2001 From: Emily Brown Date: Wed, 26 Nov 2025 03:47:11 -0800 Subject: [PATCH 2/2] Fix RCTDevLoadingView dismiss button not appearing (#54696) Summary: Changelog: [iOS][Fixed] - Fixed dismiss button not appearing consistently in dev loading view D87465522 introduced lazy initialization to reuse views across multiple `showMessage` calls for better performance. However, this exposed two bugs: 1. **Missing button bug**: Button creation was inside the `if (self->_container == nullptr)` block, which now only executes once. If the first call had `dismissButton=NO`, subsequent calls with `dismissButton=YES` would skip button creation since the container already existed. 2. **Button text wrapping bug**: The button didn't have compression resistance priority set, so Auto Layout could compress it to fit the layout, causing the text to wrap. This fixes both issues by: - Moving button creation/removal logic outside the container initialization so it runs on every call and dynamically adds or removes the button based on the current `dismissButton` parameter - Setting compression resistance and content hugging priorities on the button to prevent it from being compressed, forcing the message label to wrap instead - Resetting all UI elements in `hide()` to ensure clean state between loading sessions The performance optimization from D87465522 is preserved - views are still reused during rapid Metro progress updates. Reviewed By: javache Differential Revision: D87870932 --- .../React/CoreModules/RCTDevLoadingView.mm | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 07fe5e62ddc6..a0e67038028c 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -121,10 +121,11 @@ - (void)showMessage:(NSString *)message self->_label.translatesAutoresizingMaskIntoConstraints = NO; self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular]; self->_label.textAlignment = NSTextAlignmentCenter; + [self->_container addSubview:self->_label]; + self->_label.numberOfLines = 0; } self->_label.textColor = color; self->_label.text = message; - self->_label.numberOfLines = 0; if (self->_container == nullptr) { self->_container = [[UIView alloc] init]; @@ -132,38 +133,49 @@ - (void)showMessage:(NSString *)message UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)]; [self->_container addGestureRecognizer:tapGesture]; self->_container.userInteractionEnabled = YES; - - if (dismissButton) { - CGFloat hue = 0.0; - CGFloat saturation = 0.0; - CGFloat brightness = 0.0; - CGFloat alpha = 0.0; - [backgroundColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]; - UIColor *darkerBackground = [UIColor colorWithHue:hue - saturation:saturation - brightness:brightness * 0.7 - alpha:1.0]; - - UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration]; - buttonConfig.attributedTitle = [[NSAttributedString alloc] - initWithString:@"Dismiss ✕" - attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:11.0 weight:UIFontWeightRegular]}]; - buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(6, 12, 6, 12); - buttonConfig.background.backgroundColor = darkerBackground; - buttonConfig.background.cornerRadius = 10; - buttonConfig.baseForegroundColor = color; - - UIAction *dismissAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { - [self hide]; - }]; - self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:dismissAction]; - self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO; - [self->_container addSubview:self->_dismissButton]; - } [self->_container addSubview:self->_label]; } self->_container.backgroundColor = backgroundColor; + // Handle button creation/removal dynamically based on dismissButton parameter + if (dismissButton && self->_dismissButton == nullptr) { + CGFloat hue = 0.0; + CGFloat saturation = 0.0; + CGFloat brightness = 0.0; + CGFloat alpha = 0.0; + [backgroundColor getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]; + UIColor *darkerBackground = [UIColor colorWithHue:hue + saturation:saturation + brightness:brightness * 0.7 + alpha:1.0]; + + UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration]; + buttonConfig.attributedTitle = [[NSAttributedString alloc] + initWithString:@"Dismiss ✕" + attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:11.0 weight:UIFontWeightRegular]}]; + buttonConfig.contentInsets = NSDirectionalEdgeInsetsMake(6, 12, 6, 12); + buttonConfig.background.backgroundColor = darkerBackground; + buttonConfig.background.cornerRadius = 10; + buttonConfig.baseForegroundColor = color; + + UIAction *dismissAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { + [self hide]; + }]; + self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:dismissAction]; + self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO; + + // Prevent button from being compressed - force label to wrap instead + [self->_dismissButton setContentCompressionResistancePriority:UILayoutPriorityRequired + forAxis:UILayoutConstraintAxisHorizontal]; + [self->_dismissButton setContentHuggingPriority:UILayoutPriorityRequired + forAxis:UILayoutConstraintAxisHorizontal]; + + [self->_container addSubview:self->_dismissButton]; + } else if (!dismissButton && self->_dismissButton != nullptr) { + [self->_dismissButton removeFromSuperview]; + self->_dismissButton = nullptr; + } + UIWindow *mainWindow = RCTKeyWindow(); if (self->_window == nullptr) { UIWindowScene *windowScene = mainWindow.windowScene; @@ -245,6 +257,9 @@ - (void)showMessage:(NSString *)message self->_window.frame = windowFrame; self->_window.hidden = YES; self->_window = nil; + self->_container = nil; + self->_label = nil; + self->_dismissButton = nil; self->_hiding = false; }]; });