From a38bf378b1ce51667fdf5edce0c31130d349afb5 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Sun, 11 Oct 2026 17:46:16 -0700 Subject: [PATCH] Lazily store safe area inset state (#58959) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58959 `RCTViewComponentView` currently reserves a `UIEdgeInsets` value on every instance even though safe area observation is opt-in and rarely used. Store the last reported insets in an associated object only after a view starts emitting safe area inset changes, and clear that storage when observation stops or the view is recycled. Changelog: [Internal] Reviewed By: javache Differential Revision: D124143204 --- .../View/RCTViewComponentView.mm | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 5755da9fbb6..582557336d6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -105,9 +105,6 @@ static BOOL RCTViewIsInteractiveAccessibilityElement(UIView *view, const ViewPro } #endif -// Sentinel for insets that have not been set yet. -static const UIEdgeInsets RCTNoSafeAreaInsetsSent = {-1, -1, -1, -1}; - @implementation RCTViewComponentView { UIColor *_backgroundColor; CALayer *_backgroundColorLayer; @@ -126,7 +123,6 @@ @implementation RCTViewComponentView { NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; BOOL _focusable; - UIEdgeInsets _lastSentSafeAreaInsets; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -146,7 +142,6 @@ - (instancetype)initWithFrame:(CGRect)frame #endif _useCustomContainerView = NO; _removeClippedSubviews = NO; - _lastSentSafeAreaInsets = RCTNoSafeAreaInsetsSent; } return self; } @@ -452,7 +447,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (newViewProps.onSafeAreaInsetsChange) { [self setNeedsLayout]; } else if (oldViewProps.onSafeAreaInsetsChange) { - _lastSentSafeAreaInsets = RCTNoSafeAreaInsetsSent; + [self _setLastSentSafeAreaInsets:nil]; } // `overflow` @@ -739,6 +734,17 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics #pragma mark - Safe area insets +- (NSValue *_Nullable)_lastSentSafeAreaInsets +{ + return objc_getAssociatedObject(self, _cmd); +} + +- (void)_setLastSentSafeAreaInsets:(NSValue *_Nullable)lastSentSafeAreaInsets +{ + objc_setAssociatedObject( + self, @selector(_lastSentSafeAreaInsets), lastSentSafeAreaInsets, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + static BOOL RCTEdgeInsetsEqualWithThreshold(UIEdgeInsets lhs, UIEdgeInsets rhs, CGFloat threshold) { return ABS(lhs.left - rhs.left) <= threshold && ABS(lhs.top - rhs.top) <= threshold && @@ -762,12 +768,13 @@ - (void)_safeAreaInsetsMayHaveChanged } UIEdgeInsets insets = self.safeAreaInsets; - if (_lastSentSafeAreaInsets.top >= 0 && - RCTEdgeInsetsEqualWithThreshold(insets, _lastSentSafeAreaInsets, 1.0 / RCTScreenScale())) { + NSValue *lastSentSafeAreaInsets = [self _lastSentSafeAreaInsets]; + if (lastSentSafeAreaInsets != nil && + RCTEdgeInsetsEqualWithThreshold(insets, lastSentSafeAreaInsets.UIEdgeInsetsValue, 1.0 / RCTScreenScale())) { return; } - _lastSentSafeAreaInsets = insets; + [self _setLastSentSafeAreaInsets:[NSValue valueWithUIEdgeInsets:insets]]; static_cast(*_eventEmitter) .onSafeAreaInsetsChange( @@ -864,7 +871,7 @@ - (void)prepareForRecycle _filterLayer = nil; [self clearExistingBackgroundImageLayers]; - _lastSentSafeAreaInsets = RCTNoSafeAreaInsetsSent; + [self _setLastSentSafeAreaInsets:nil]; _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; _eventEmitter.reset(); _isJSResponder = NO;