@@ -123,9 +123,11 @@ @implementation RCTViewComponentView {
123123 NSMutableSet <NSString *> *_accessibilityOrderNativeIDs;
124124 RCTSwiftUIContainerViewWrapper *_swiftUIWrapper;
125125 BOOL _focusable;
126- BOOL _observesSafeAreaInsets;
127- BOOL _safeAreaInsetsWereSent;
128- UIEdgeInsets _lastSafeAreaInsets;
126+ // The insets sent with the last `onSafeAreaInsetsChange` event, or nil if
127+ // none was sent yet. A pointer because almost no view observes the safe
128+ // area: the views that do pay for a small box, every other view only for
129+ // the pointer.
130+ NSValue *_lastSentSafeAreaInsets;
129131}
130132
131133#ifdef RCT_DYNAMIC_FRAMEWORKS
@@ -442,10 +444,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
442444 -newViewProps.hitSlop .right };
443445 }
444446
445- // `onSafeAreaInsetsChange`. Compared against the current observation state
446- // rather than `oldViewProps`: recycled views keep their last props, so the
447- // old props of a freshly reused view are not a reliable baseline.
448- [self _setObservesSafeAreaInsets: newViewProps.onSafeAreaInsetsChange];
447+ // `onSafeAreaInsetsChange`. Scheduled whenever the prop is set, not only on
448+ // its transitions: recycled views keep their last props, so `oldViewProps`
449+ // of a freshly reused view is not a reliable baseline.
450+ if (newViewProps.onSafeAreaInsetsChange ) {
451+ [self setNeedsLayout ];
452+ } else if (oldViewProps.onSafeAreaInsetsChange ) {
453+ _lastSentSafeAreaInsets = nil ;
454+ }
449455
450456 // `overflow`
451457 if (oldViewProps.getClipsContentToBounds () != newViewProps.getClipsContentToBounds ()) {
@@ -731,19 +737,6 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
731737
732738#pragma mark - Safe area insets
733739
734- #if !TARGET_OS_TV
735- static NSArray <NSNotificationName > *RCTSafeAreaInsetsNotificationNames (void )
736- {
737- // The keyboard is part of the safe area on iOS, so its appearance changes the
738- // insets without the view hierarchy itself changing.
739- return @[
740- UIKeyboardDidShowNotification,
741- UIKeyboardDidHideNotification,
742- UIKeyboardDidChangeFrameNotification,
743- ];
744- }
745- #endif
746-
747740// The view controller the view is hosted in, which is the coordinate space
748741// `frame` is reported in. Modals and other view controllers are positioned
749742// independently of the window, so the window is not a usable reference.
@@ -765,47 +758,15 @@ static BOOL RCTEdgeInsetsEqualWithThreshold(UIEdgeInsets lhs, UIEdgeInsets rhs,
765758 ABS (lhs.right - rhs.right ) <= threshold && ABS (lhs.bottom - rhs.bottom ) <= threshold;
766759}
767760
768- - (void )_setObservesSafeAreaInsets : (BOOL )observesSafeAreaInsets
769- {
770- if (_observesSafeAreaInsets == observesSafeAreaInsets) {
771- return ;
772- }
773-
774- _observesSafeAreaInsets = observesSafeAreaInsets;
775- _safeAreaInsetsWereSent = NO ;
776-
777- #if !TARGET_OS_TV
778- for (NSNotificationName name in RCTSafeAreaInsetsNotificationNames ()) {
779- if (observesSafeAreaInsets) {
780- [NSNotificationCenter .defaultCenter addObserver: self
781- selector: @selector (_scheduleSafeAreaInsetsCheck )
782- name: name
783- object: nil ];
784- } else {
785- [NSNotificationCenter .defaultCenter removeObserver: self name: name object: nil ];
786- }
787- }
788- #endif
789-
790- if (observesSafeAreaInsets) {
791- [self _scheduleSafeAreaInsetsCheck ];
792- }
793- }
794-
795761// The event is only ever emitted from `layoutSubviews`; everything that might
796- // have changed the insets funnels through here and merely marks the view as
797- // needing layout. This defers the emit out of arbitrary call contexts — in
798- // particular out of `updateProps`, which runs inside the mounting transaction
799- // where synchronously re-entering React is not safe — while keeping it in the
800- // same frame: the layout pass runs before the frame is displayed.
801- - (void )_scheduleSafeAreaInsetsCheck
802- {
803- [self setNeedsLayout ];
804- }
805-
762+ // have changed the insets merely marks the view as needing layout. This defers
763+ // the emit out of arbitrary call contexts — in particular out of
764+ // `updateProps`, which runs inside the mounting transaction where
765+ // synchronously re-entering React is not safe — while keeping it in the same
766+ // frame: the layout pass runs before the frame is displayed.
806767- (void )_safeAreaInsetsMayHaveChanged
807768{
808- if (!_observesSafeAreaInsets || ! _eventEmitter) {
769+ if (!_eventEmitter) {
809770 return ;
810771 }
811772
@@ -820,16 +781,15 @@ - (void)_safeAreaInsetsMayHaveChanged
820781 // the system UI changing stays silent, which is what makes observing views
821782 // safe to place inside scroll views.
822783 UIEdgeInsets insets = self.safeAreaInsets ;
823- if (_safeAreaInsetsWereSent &&
824- RCTEdgeInsetsEqualWithThreshold (insets, _lastSafeAreaInsets , 1.0 / RCTScreenScale ())) {
784+ if (_lastSentSafeAreaInsets != nil &&
785+ RCTEdgeInsetsEqualWithThreshold (insets, _lastSentSafeAreaInsets. UIEdgeInsetsValue , 1.0 / RCTScreenScale ())) {
825786 return ;
826787 }
827788
828789 UIView *referenceView = RCTParentViewControllerOfView (self).view ?: self.window ;
829790 CGRect frame = [self convertRect: self .bounds toView: referenceView];
830791
831- _safeAreaInsetsWereSent = YES ;
832- _lastSafeAreaInsets = insets;
792+ _lastSentSafeAreaInsets = [NSValue valueWithUIEdgeInsets: insets];
833793
834794 static_cast <const ViewEventEmitter &>(*_eventEmitter)
835795 .onSafeAreaInsetsChange (
@@ -841,21 +801,26 @@ - (void)_safeAreaInsetsMayHaveChanged
841801 RCTRectFromCGRect (frame));
842802}
843803
804+ // The prop is checked here rather than inside the helper so that views which
805+ // do not use it only pay for a branch on a prop they already have in hand.
806+ - (BOOL )_observesSafeAreaInsets
807+ {
808+ return static_cast <const ViewProps &>(*_props).onSafeAreaInsetsChange ;
809+ }
810+
844811- (void )safeAreaInsetsDidChange
845812{
846813 [super safeAreaInsetsDidChange ];
847- // The ivar is checked here rather than inside the helpers so that views which
848- // do not use the prop only pay for a branch.
849- if (_observesSafeAreaInsets) {
850- [self _scheduleSafeAreaInsetsCheck ];
814+ if ([self _observesSafeAreaInsets ]) {
815+ [self setNeedsLayout ];
851816 }
852817}
853818
854819- (void )didMoveToWindow
855820{
856821 [super didMoveToWindow ];
857- if (_observesSafeAreaInsets) {
858- [self _scheduleSafeAreaInsetsCheck ];
822+ if ([ self _observesSafeAreaInsets ] ) {
823+ [self setNeedsLayout ];
859824 }
860825}
861826
@@ -864,7 +829,7 @@ - (void)layoutSubviews
864829 [super layoutSubviews ];
865830 // Both the insets and the frame depend on where the view sits in the window,
866831 // so moving or resizing it changes them without UIKit notifying us.
867- if (_observesSafeAreaInsets) {
832+ if ([ self _observesSafeAreaInsets ] ) {
868833 [self _safeAreaInsetsMayHaveChanged ];
869834 }
870835}
@@ -924,8 +889,7 @@ - (void)prepareForRecycle
924889 _filterLayer = nil ;
925890 [self clearExistingBackgroundImageLayers ];
926891
927- [self _setObservesSafeAreaInsets: NO ];
928-
892+ _lastSentSafeAreaInsets = nil ;
929893 _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil ;
930894 _eventEmitter.reset ();
931895 _isJSResponder = NO ;
0 commit comments