diff --git a/packages/react-native/Libraries/Utilities/DevLoadingView.js b/packages/react-native/Libraries/Utilities/DevLoadingView.js index d17e25146e14..5aaaf133647d 100644 --- a/packages/react-native/Libraries/Utilities/DevLoadingView.js +++ b/packages/react-native/Libraries/Utilities/DevLoadingView.js @@ -19,7 +19,7 @@ const COLOR_SCHEME = { textColor: '#242526', }, refresh: { - backgroundColor: '#2584e8', + backgroundColor: '#1065AF', textColor: '#ffffff', }, error: { @@ -33,7 +33,7 @@ const COLOR_SCHEME = { textColor: '#ffffff', }, refresh: { - backgroundColor: '#2584e8', + backgroundColor: '#1065AF', textColor: '#ffffff', }, error: { diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index 5b3a91f2ac9f..bf3e7826ddb8 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -323,7 +323,8 @@ function setHMRUnavailableReason(reason: string) { // the warning to native engineers who use cached bundles without Metro. if (hmrClient.isEnabled() && didConnect) { DevLoadingView.showMessage( - 'Fast Refresh disconnected. Reload app to reconnect.', + // Keeps 'to reconnect.' together when the banner wraps. + 'Fast Refresh disconnected. Reload app to\u00A0reconnect.', 'error', {dismissButton: true}, ); diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 94a22481935e..37c570e9006f 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -28,14 +28,80 @@ @interface RCTDevLoadingView () #if RCT_DEV_MENU +static const CGFloat RCTDevLoadingViewTopMargin = 16; +static const CGFloat RCTDevLoadingViewHorizontalMargin = 16; +static const CGFloat RCTDevLoadingViewLabelHorizontalPadding = 14; +static const CGFloat RCTDevLoadingViewLabelVerticalPadding = 10; +static const CGFloat RCTDevLoadingViewMinHeight = 40; +static const CGFloat RCTDevLoadingViewDismissButtonSize = 22; +static const CGFloat RCTDevLoadingViewFontSize = 12.5; +static const CGFloat RCTDevLoadingViewLineSpacing = 2; +static const NSTimeInterval RCTDevLoadingViewAnimationDuration = 0.2; +static const CGFloat RCTDevLoadingViewHiddenScale = 0.85; +// The hidden state scales about a point this fraction of the capsule's height above its center. +static const CGFloat RCTDevLoadingViewHiddenAnchorOffset = 0.05; + +#if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1 +#define RCT_DEV_LOADING_VIEW_HAS_HINGE 1 +#else +#define RCT_DEV_LOADING_VIEW_HAS_HINGE 0 +#endif + +// The horizontal offset from the window's center that matches where a system alert centers itself. Alerts don't +// center on the window or on its safe area, but between the two: 0.2818 of the way toward the safe area's center, +// which is 11.8pt with the iPhone Duo's 84pt rail. No API reports this, so the fraction is measured from a +// UIAlertController on iOS 27.1. +static CGFloat RCTDevLoadingViewAlertCenterOffset(UIEdgeInsets safeAreaInsets) +{ + return 0.2818 * (safeAreaInsets.left - safeAreaInsets.right) / 2; +} + +// Hosts the banner above the app's windows and passes touches outside it through to them. +@interface RCTDevLoadingWindow : UIWindow +// Called after each layout pass, since the banner's placement depends on the window's size and safe area in +// ways Auto Layout can't express. +@property (nonatomic, copy) void (^layoutHandler)(void); +@end + +@implementation RCTDevLoadingWindow + +- (void)layoutSubviews +{ + [super layoutSubviews]; + if (_layoutHandler != nil) { + _layoutHandler(); + } +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + UIView *view = [super hitTest:point withEvent:event]; + return view == self.rootViewController.view ? nil : view; +} + +@end + @implementation RCTDevLoadingView { UIWindow *_window; UILabel *_label; UIView *_container; + UIView *_contentView; UIButton *_dismissButton; + NSLayoutConstraint *_labelTrailingConstraint; + NSLayoutConstraint *_labelToButtonConstraint; + NSLayoutConstraint *_centerConstraint; + NSLayoutConstraint *_trailingLimitConstraint; + NSLayoutConstraint *_topSafeAreaConstraint; NSDate *_showDate; BOOL _hiding; + // Incremented by each show so a hide animation that it interrupts does not tear down the new banner. + NSUInteger _generation; dispatch_block_t _initialMessageBlock; +#if RCT_DEV_LOADING_VIEW_HAS_HINGE + NSTimer *_divisionPollTimer; + NSDate *_divisionPollDeadline; + BOOL _hingeOpen; +#endif } RCT_EXPORT_MODULE() @@ -62,6 +128,9 @@ - (instancetype)init - (void)dealloc { [self clearInitialMessageDelay]; +#if RCT_DEV_LOADING_VIEW_HAS_HINGE + [_divisionPollTimer invalidate]; +#endif [[NSNotificationCenter defaultCenter] removeObserver:self]; UIWindow *window = _window; _window = nil; @@ -108,7 +177,7 @@ - (void)showMessage:(NSString *)message backgroundColor:(UIColor *)backgroundColor dismissButton:(BOOL)dismissButton { - if (!RCTDevLoadingViewGetEnabled() || _hiding) { + if (!RCTDevLoadingViewGetEnabled()) { return; } @@ -132,119 +201,328 @@ - (void)showMessage:(NSString *)message } self->_showDate = [NSDate date]; - - if (self->_label == nullptr) { - self->_label = [[UILabel alloc] init]; - 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; - - if (self->_container == nullptr) { - self->_container = [[UIView alloc] init]; - self->_container.translatesAutoresizingMaskIntoConstraints = NO; - UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)]; - [self->_container addGestureRecognizer:tapGesture]; - self->_container.userInteractionEnabled = YES; - [self->_container addSubview:self->_label]; + self->_generation++; + if (self->_hiding) { + [self _removeBanner]; } - 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; + if (self->_window == nullptr) { + UIWindowScene *windowScene = RCTKeyWindow().windowScene; + self->_window = windowScene != nil ? [[RCTDevLoadingWindow alloc] initWithWindowScene:windowScene] + : [[RCTDevLoadingWindow alloc] init]; + self->_window.frame = self->_window.windowScene.coordinateSpace.bounds; +#if !TARGET_OS_TV + self->_window.windowLevel = UIWindowLevelStatusBar + 1; +#endif + self->_window.rootViewController = [UIViewController new]; + __weak __typeof(self) weakSelf = self; + ((RCTDevLoadingWindow *)self->_window).layoutHandler = ^{ + [weakSelf _updatePlacementAnimated:NO]; + }; + self->_window.rootViewController.view.backgroundColor = UIColor.clearColor; +#if RCT_DEV_LOADING_VIEW_HAS_HINGE + [self _observeHinge]; +#endif + } - // Button is a visual cue to tap anywhere on the banner to dismiss so no separate action is needed - self->_dismissButton = [UIButton buttonWithConfiguration:buttonConfig primaryAction:nil]; - self->_dismissButton.userInteractionEnabled = NO; - self->_dismissButton.translatesAutoresizingMaskIntoConstraints = NO; + BOOL isNewBanner = self->_container == nullptr; + if (isNewBanner) { + [self _createBannerWithBackgroundColor:backgroundColor]; + } else { + [self _applyBackgroundColor:backgroundColor]; + } - // Prevent button from being compressed - force label to wrap instead - [self->_dismissButton setContentCompressionResistancePriority:UILayoutPriorityRequired - forAxis:UILayoutConstraintAxisHorizontal]; - [self->_dismissButton setContentHuggingPriority:UILayoutPriorityRequired - forAxis:UILayoutConstraintAxisHorizontal]; + self->_label.textColor = color; + [self _setLabelText:message]; - [self->_container addSubview:self->_dismissButton]; + if (dismissButton && self->_dismissButton == nullptr) { + [self _createDismissButton]; } else if (!dismissButton && self->_dismissButton != nullptr) { [self->_dismissButton removeFromSuperview]; self->_dismissButton = nullptr; + self->_labelToButtonConstraint = nil; + } + if (self->_dismissButton != nullptr) { + UIButtonConfiguration *buttonConfig = self->_dismissButton.configuration; + buttonConfig.baseForegroundColor = color; + buttonConfig.background.backgroundColor = [color colorWithAlphaComponent:0.2]; + self->_dismissButton.configuration = buttonConfig; } + self->_labelTrailingConstraint.active = self->_dismissButton == nullptr; + self->_labelToButtonConstraint.active = self->_dismissButton != nullptr; - UIWindow *mainWindow = RCTKeyWindow(); - if (self->_window == nullptr) { - UIWindowScene *windowScene = mainWindow.windowScene; - if (windowScene != nil) { - self->_window = [[UIWindow alloc] initWithWindowScene:windowScene]; - } else { - self->_window = [[UIWindow alloc] init]; - } + self->_window.hidden = NO; + [self _updatePlacementAnimated:NO]; + [self->_window layoutIfNeeded]; + if (isNewBanner) { + [self _animateBannerIn]; + } + }); +} - self->_window.frame = self->_window.windowScene.coordinateSpace.bounds; +#if RCT_DEV_LOADING_VIEW_HAS_HINGE +- (void)_observeHinge +{ + if (@available(iOS 27.1, *)) { + __weak __typeof(self) weakSelf = self; + UIHingeInteraction *interaction = [[UIHingeInteraction alloc] + initWithUpdateHandler:^(__unused UIHingeInteraction *hingeInteraction, UIHingeInteractionUpdate *update) { + [weakSelf _hingeDidUpdate:update.hinge]; + }]; + [_window.rootViewController.view addInteraction:interaction]; + } +} -#if !TARGET_OS_TV - self->_window.windowLevel = UIWindowLevelStatusBar + 1; +// The division region becomes active once the hinge settles, matching when the system's own UI moves aside, +// but that change has no notification of its own, so each hinge update starts a short poll for it. +- (void)_hingeDidUpdate:(UIHinge *)hinge API_AVAILABLE(ios(27.1)) +{ + _hingeOpen = hinge.status == UIHingeStatusPartiallyOpen || hinge.status == UIHingeStatusFullyOpen; + _divisionPollDeadline = [NSDate dateWithTimeIntervalSinceNow:1.5]; + if (_divisionPollTimer != nil) { + return; + } + __weak __typeof(self) weakSelf = self; + _divisionPollTimer = + [NSTimer scheduledTimerWithTimeInterval:0.05 + repeats:YES + block:^(NSTimer *timer) { + __typeof(self) strongSelf = weakSelf; + if (strongSelf == nil) { + [timer invalidate]; + return; + } + [strongSelf _updatePlacementAnimated:YES]; + if (strongSelf->_divisionPollDeadline.timeIntervalSinceNow < 0) { + [timer invalidate]; + strongSelf->_divisionPollTimer = nil; + } + }]; +} #endif - self->_window.rootViewController = [UIViewController new]; - [self->_window.rootViewController.view addSubview:self->_container]; + +// Places the banner where a system alert would center itself, or while a fold divides the screen, centered within +// the leading segment and clear of the fold. On a foldable held open in portrait, the status bar sits in a corner +// while its inset spans the whole top edge, so the banner ignores that inset and sits 16pt from the top. +- (void)_updatePlacementAnimated:(BOOL)animated +{ + if (_container == nil) { + return; + } + UIView *rootView = _window.rootViewController.view; + UIEdgeInsets safeAreaInsets = rootView.safeAreaInsets; + CGFloat centerOffset = RCTDevLoadingViewAlertCenterOffset(safeAreaInsets); + CGFloat trailingMargin = RCTDevLoadingViewHorizontalMargin; + BOOL respectsTopSafeArea = YES; +#if RCT_DEV_LOADING_VIEW_HAS_HINGE + if (@available(iOS 27.1, *)) { + respectsTopSafeArea = !(_hingeOpen && CGRectGetHeight(rootView.bounds) > CGRectGetWidth(rootView.bounds)); + CGFloat minX = safeAreaInsets.left; + CGFloat maxX = CGRectGetWidth(rootView.bounds) - safeAreaInsets.right; + for (UIViewReservedRegion *region in + [rootView reservedRegionsOfKind:[UIViewReservedRegionKind divisionRegionKind]]) { + CGFloat dividerX = CGRectGetMinX(region.frame); + if (region.active && CGRectGetHeight(region.frame) >= CGRectGetWidth(region.frame) && dividerX > minX && + dividerX < maxX) { + centerOffset = (minX + dividerX) / 2 - CGRectGetMidX(rootView.bounds); + trailingMargin += maxX - dividerX; + } } + } +#endif + if (centerOffset == _centerConstraint.constant && -trailingMargin == _trailingLimitConstraint.constant && + respectsTopSafeArea == _topSafeAreaConstraint.active) { + return; + } + _centerConstraint.constant = centerOffset; + _trailingLimitConstraint.constant = -trailingMargin; + _topSafeAreaConstraint.active = respectsTopSafeArea; + if (animated) { + [UIView animateWithDuration:RCTDevLoadingViewAnimationDuration + animations:^{ + [rootView layoutIfNeeded]; + }]; + } +} - CGFloat topSafeAreaHeight = mainWindow.safeAreaInsets.top; - self->_window.hidden = NO; +- (CGAffineTransform)_hiddenTransform +{ + CGFloat anchorY = -RCTDevLoadingViewHiddenAnchorOffset * _container.bounds.size.height; + CGAffineTransform transform = CGAffineTransformMakeTranslation(0, anchorY); + transform = CGAffineTransformScale(transform, RCTDevLoadingViewHiddenScale, RCTDevLoadingViewHiddenScale); + return CGAffineTransformTranslate(transform, 0, -anchorY); +} - [self->_window layoutIfNeeded]; +- (void)_animateBannerIn +{ + // A visual effect view renders incorrectly below full alpha, so its effect and content fade instead. + UIVisualEffectView *effectView = + [_container isKindOfClass:UIVisualEffectView.class] ? (UIVisualEffectView *)_container : nil; + UIVisualEffect *effect = effectView.effect; + effectView.effect = nil; + if (effectView != nil) { + _contentView.alpha = 0; + } else { + _container.alpha = 0; + } + _container.transform = [self _hiddenTransform]; + + [UIView animateWithDuration:RCTDevLoadingViewAnimationDuration + delay:0 + options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction + animations:^{ + effectView.effect = effect; + self->_contentView.alpha = 1; + self->_container.alpha = 1; + self->_container.transform = CGAffineTransformIdentity; + } + completion:nil]; +} - NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[ - // Container constraints - [self->_container.topAnchor constraintEqualToAnchor:self->_window.rootViewController.view.topAnchor], - [self->_container.leadingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.leadingAnchor], - [self->_container.trailingAnchor constraintEqualToAnchor:self->_window.rootViewController.view.trailingAnchor], +- (void)_createBannerWithBackgroundColor:(UIColor *)backgroundColor +{ + UIView *container; + UIView *contentView; +#if defined(__IPHONE_26_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_26_0 + if (@available(iOS 26.0, *)) { + UIVisualEffectView *effectView = + [[UIVisualEffectView alloc] initWithEffect:[self _glassEffectWithTintColor:backgroundColor]]; + effectView.cornerConfiguration = [UICornerConfiguration capsuleConfiguration]; + container = effectView; + contentView = effectView.contentView; + } +#endif + if (container == nil) { + container = [UIView new]; + container.backgroundColor = backgroundColor; + container.layer.cornerRadius = RCTDevLoadingViewMinHeight / 2; + container.layer.cornerCurve = kCACornerCurveContinuous; + container.clipsToBounds = YES; + contentView = container; + } + container.translatesAutoresizingMaskIntoConstraints = NO; + [container addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)]]; + + UILabel *label = [UILabel new]; + label.translatesAutoresizingMaskIntoConstraints = NO; + label.font = [UIFont monospacedDigitSystemFontOfSize:RCTDevLoadingViewFontSize weight:UIFontWeightMedium]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + [contentView addSubview:label]; + + UIView *rootView = _window.rootViewController.view; + [rootView addSubview:container]; + + // Centered on the window like a system alert, with required safe area limits that only push the banner + // inward where the two would overlap, such as beside the camera cutout. + UILayoutGuide *safeArea = rootView.safeAreaLayoutGuide; + _centerConstraint = [container.centerXAnchor constraintEqualToAnchor:rootView.centerXAnchor]; + _centerConstraint.priority = UILayoutPriorityDefaultHigh; + _trailingLimitConstraint = + [container.trailingAnchor constraintLessThanOrEqualToAnchor:safeArea.trailingAnchor + constant:-RCTDevLoadingViewHorizontalMargin]; + _labelTrailingConstraint = [label.trailingAnchor constraintEqualToAnchor:contentView.trailingAnchor + constant:-RCTDevLoadingViewLabelHorizontalPadding]; + // Sits flush under a top safe area inset such as the status bar, or 16pt from the top edge without one. + _topSafeAreaConstraint = [container.topAnchor constraintGreaterThanOrEqualToAnchor:safeArea.topAnchor]; + NSLayoutConstraint *topPull = [container.topAnchor constraintEqualToAnchor:rootView.topAnchor]; + topPull.priority = UILayoutPriorityDefaultLow; + [NSLayoutConstraint activateConstraints:@[ + _centerConstraint, + _trailingLimitConstraint, + [container.leadingAnchor constraintGreaterThanOrEqualToAnchor:safeArea.leadingAnchor + constant:RCTDevLoadingViewHorizontalMargin], + _topSafeAreaConstraint, + [container.topAnchor constraintGreaterThanOrEqualToAnchor:rootView.topAnchor constant:RCTDevLoadingViewTopMargin], + topPull, + [container.heightAnchor constraintGreaterThanOrEqualToConstant:RCTDevLoadingViewMinHeight], + [label.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:RCTDevLoadingViewLabelVerticalPadding], + [label.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor + constant:-RCTDevLoadingViewLabelVerticalPadding], + [label.leadingAnchor constraintEqualToAnchor:contentView.leadingAnchor + constant:RCTDevLoadingViewLabelHorizontalPadding], + ]]; + + _container = container; + _contentView = contentView; + _label = label; +} - // Label constraints - [self->_label.topAnchor constraintEqualToAnchor:self->_container.topAnchor constant:topSafeAreaHeight + 8], - [self->_label.leadingAnchor constraintEqualToAnchor:self->_container.leadingAnchor constant:10], - [self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-8], - ]]; +- (void)_setLabelText:(NSString *)message +{ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.alignment = NSTextAlignmentCenter; + paragraphStyle.lineSpacing = RCTDevLoadingViewLineSpacing; + _label.attributedText = [[NSAttributedString alloc] initWithString:message + attributes:@{NSParagraphStyleAttributeName : paragraphStyle}]; +} - // Add button-specific constraints if button exists - if (self->_dismissButton != nullptr) { - [constraints addObjectsFromArray:@[ - [self->_dismissButton.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor constant:-10], - [self->_dismissButton.centerYAnchor constraintEqualToAnchor:self->_label.centerYAnchor], - [self->_dismissButton.heightAnchor constraintEqualToConstant:22], - [self->_label.trailingAnchor constraintEqualToAnchor:self->_dismissButton.leadingAnchor constant:-10], - ]]; - } else { - [constraints addObject:[self->_label.trailingAnchor constraintEqualToAnchor:self->_container.trailingAnchor - constant:-10]]; - } +- (void)_createDismissButton +{ + UIButtonConfiguration *buttonConfig = [UIButtonConfiguration plainButtonConfiguration]; + buttonConfig.image = [UIImage + systemImageNamed:@"xmark" + withConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:10 weight:UIImageSymbolWeightBold]]; + buttonConfig.contentInsets = NSDirectionalEdgeInsetsZero; + buttonConfig.cornerStyle = UIButtonConfigurationCornerStyleCapsule; + + __weak __typeof(self) weakSelf = self; + UIButton *button = [UIButton buttonWithConfiguration:buttonConfig + primaryAction:[UIAction actionWithHandler:^(__unused UIAction *action) { + [weakSelf hide]; + }]]; + button.accessibilityLabel = @"Dismiss"; + button.translatesAutoresizingMaskIntoConstraints = NO; + [button setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; + [_contentView addSubview:button]; + + _labelToButtonConstraint = [_label.trailingAnchor constraintEqualToAnchor:button.leadingAnchor constant:-8]; + [NSLayoutConstraint activateConstraints:@[ + [button.trailingAnchor constraintEqualToAnchor:_contentView.trailingAnchor constant:-9], + [button.centerYAnchor constraintEqualToAnchor:_contentView.centerYAnchor], + [button.widthAnchor constraintEqualToConstant:RCTDevLoadingViewDismissButtonSize], + [button.heightAnchor constraintEqualToConstant:RCTDevLoadingViewDismissButtonSize], + ]]; + + _dismissButton = button; +} - [NSLayoutConstraint activateConstraints:constraints]; +- (void)_removeBanner +{ + [_container removeFromSuperview]; + _container = nil; + _contentView = nil; + _label = nil; + _dismissButton = nil; + _labelTrailingConstraint = nil; + _labelToButtonConstraint = nil; + _centerConstraint = nil; + _trailingLimitConstraint = nil; + _topSafeAreaConstraint = nil; + _hiding = NO; +} - [self->_window layoutIfNeeded]; - }); +- (void)_applyBackgroundColor:(UIColor *)backgroundColor +{ +#if defined(__IPHONE_26_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_26_0 + if (@available(iOS 26.0, *)) { + if ([_container isKindOfClass:UIVisualEffectView.class]) { + ((UIVisualEffectView *)_container).effect = [self _glassEffectWithTintColor:backgroundColor]; + return; + } + } +#endif + _container.backgroundColor = backgroundColor; +} + +#if defined(__IPHONE_26_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_26_0 +- (UIGlassEffect *)_glassEffectWithTintColor:(UIColor *)tintColor API_AVAILABLE(ios(26.0)) +{ + UIGlassEffect *effect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleRegular]; + effect.tintColor = tintColor; + return effect; } +#endif - (void)showMessage:(NSString *)message withColor:(NSNumber *__nonnull)color @@ -267,24 +545,29 @@ - (void)hide dispatch_async(dispatch_get_main_queue(), ^{ self->_hiding = YES; + NSUInteger generation = self->_generation; const NSTimeInterval MIN_PRESENTED_TIME = 0.6; NSTimeInterval presentedTime = [[NSDate date] timeIntervalSinceDate:self->_showDate]; NSTimeInterval delay = MAX(0, MIN_PRESENTED_TIME - presentedTime); - CGFloat height = self->_container.bounds.size.height; - [UIView animateWithDuration:0.25 + [UIView animateWithDuration:RCTDevLoadingViewAnimationDuration delay:delay - options:0 + options:UIViewAnimationOptionCurveEaseIn animations:^{ - self->_container.transform = CGAffineTransformMakeTranslation(0, -height); + if ([self->_container isKindOfClass:UIVisualEffectView.class]) { + ((UIVisualEffectView *)self->_container).effect = nil; + self->_contentView.alpha = 0; + } else { + self->_container.alpha = 0; + } + self->_container.transform = [self _hiddenTransform]; } completion:^(__unused BOOL finished) { - self->_container.transform = CGAffineTransformIdentity; + if (self->_generation != generation) { + return; + } + [self _removeBanner]; self->_window.hidden = YES; self->_window = nil; - self->_container = nil; - self->_label = nil; - self->_dismissButton = nil; - self->_hiding = false; }]; }); } @@ -295,7 +578,7 @@ - (void)showProgressMessage:(NSString *)message // This is an optimization. Since the progress can come in quickly, // we want to do the minimum amount of work to update the UI, // which is to only update the label text. - _label.text = message; + [self _setLabelText:message]; return; }