From a5a577a74f16e5b0ab244eb6845432ae1d83265b Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Sun, 21 Jun 2026 15:04:59 +0300 Subject: [PATCH] fix(ios): default contentInsetAdjustmentBehavior to scrollableAxes on iOS 26+ On iOS 26+, Apple introduced the liquid glass design language with translucent system chrome (tab bars, toolbars). Scroll views need contentInsetAdjustmentBehavior set to scrollableAxes to automatically adjust content insets for this translucent chrome. This change upgrades the default from "never" to "scrollableAxes" on iOS 26+ when UIDesignRequiresCompatibility is not YES, affecting: - RCTEnhancedScrollView initWithFrame: - RCTScrollViewComponentView updateProps: (upgrades JS default "never") - RCTScrollViewComponentView prepareForRecycle Apps that explicitly set contentInsetAdjustmentBehavior to "automatic", "always", or "scrollableAxes" from JS are unaffected. Apps with UIDesignRequiresCompatibility=YES retain the current "never" default. Fixes https://github.com/react/react-native/issues/57299 Co-authored-by: Cursor --- .../ScrollView/RCTEnhancedScrollView.mm | 14 ++++++++---- .../ScrollView/RCTScrollViewComponentView.mm | 22 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm index b3481c1b98b4..03c9ce7fadcf 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm @@ -31,10 +31,16 @@ + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - // We set the default behavior to "never" so that iOS - // doesn't do weird things to UIScrollView insets automatically - // and keeps it as an opt-in behavior. - self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + if (@available(iOS 26, *)) { + NSNumber *compat = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"]; + if (!compat.boolValue) { + self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentScrollableAxes; + } else { + self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + } else { + self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } // We intentionally force `UIScrollView`s `semanticContentAttribute` to `LTR` here // because this attribute affects a position of vertical scrollbar; we don't want this diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 548987ff291d..c8348cd539cc 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -436,7 +436,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if ((oldScrollViewProps.contentInsetAdjustmentBehavior != newScrollViewProps.contentInsetAdjustmentBehavior) || _shouldUpdateContentInsetAdjustmentBehavior) { - const auto contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior; + auto contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior; + if (@available(iOS 26, *)) { + NSNumber *compat = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"]; + if (!compat.boolValue && contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) { + contentInsetAdjustmentBehavior = ContentInsetAdjustmentBehavior::ScrollableAxes; + } + } if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) { scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Automatic) { @@ -698,10 +704,16 @@ - (void)prepareForRecycle _contentSize = CGSizeZero; // Reset contentInset to prevent stale insets leaking into recycled scroll views. _scrollView.contentInset = UIEdgeInsetsZero; - // We set the default behavior to "never" so that iOS - // doesn't do weird things to UIScrollView insets automatically - // and keeps it as an opt-in behavior. - _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + if (@available(iOS 26, *)) { + NSNumber *compat = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"]; + if (!compat.boolValue) { + _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentScrollableAxes; + } else { + _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + } else { + _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } _shouldUpdateContentInsetAdjustmentBehavior = YES; _isUserTriggeredScrolling = NO; CGRect oldFrame = self.frame;