Repository navigation
[ScrollView][iOS] Fix contentInsetAdjustmentBehavior for the horizontal safe area (iPhone Duo) #59194
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[ScrollView][iOS] Fix contentInsetAdjustmentBehavior for the horizontal safe area (iPhone Duo) #59194
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| #import <UIKit/UIKit.h> | ||
|
|
||
| #import <React/RCTViewComponentView.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| /* | ||
| * UIView class for the <ScrollContentView> component, the content container of a <ScrollView>. | ||
| */ | ||
| @interface RCTScrollContentViewComponentView : RCTViewComponentView | ||
|
|
||
| /* | ||
| * The safe area the content is laid out clear of, as padding on top of the content container's own. The enclosing | ||
| * scroll view sets the pair of insets across its scrollable axis, and zero while its `contentInsetAdjustmentBehavior` | ||
| * is `never`. | ||
| */ | ||
| @property (nonatomic, assign) UIEdgeInsets safeAreaPadding; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| #import "RCTScrollContentViewComponentView.h" | ||
|
|
||
| #import <React/RCTI18nUtil.h> | ||
| #import <React/RCTUtils.h> | ||
| #import <react/renderer/components/scrollview/ScrollContentViewComponentDescriptor.h> | ||
| #import <react/renderer/components/scrollview/ScrollContentViewState.h> | ||
| #import "RCTConversions.h" | ||
| #import "RCTFabricComponentsPlugins.h" | ||
|
|
||
| using namespace facebook::react; | ||
|
|
||
| @implementation RCTScrollContentViewComponentView { | ||
| ScrollContentViewShadowNode::ConcreteState::Shared _state; | ||
| } | ||
|
|
||
| - (instancetype)initWithFrame:(CGRect)frame | ||
| { | ||
| if (self = [super initWithFrame:frame]) { | ||
| _props = ScrollContentViewShadowNode::defaultSharedProps(); | ||
| } | ||
|
|
||
| return self; | ||
| } | ||
|
|
||
| - (void)setSafeAreaPadding:(UIEdgeInsets)safeAreaPadding | ||
| { | ||
| _safeAreaPadding = safeAreaPadding; | ||
| [self _updateStateIfNecessary]; | ||
| } | ||
|
|
||
| - (void)_updateStateIfNecessary | ||
| { | ||
| if (!_state) { | ||
| return; | ||
| } | ||
|
|
||
| UIEdgeInsets insets = _safeAreaPadding; | ||
| insets.left = RCTRoundPixelValue(insets.left); | ||
| insets.top = RCTRoundPixelValue(insets.top); | ||
| insets.right = RCTRoundPixelValue(insets.right); | ||
| insets.bottom = RCTRoundPixelValue(insets.bottom); | ||
|
|
||
| auto newPadding = RCTEdgeInsetsFromUIEdgeInsets(insets); | ||
| RCTI18nUtil *i18nUtil = [RCTI18nUtil sharedInstance]; | ||
| auto layoutDirection = i18nUtil.isRTL ? LayoutDirection::RightToLeft : LayoutDirection::LeftToRight; | ||
|
Comment on lines
+51
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. his takes the direction from the global RTL setting. If a subtree sets |
||
| bool swapLeftAndRightInRTL = i18nUtil.isRTL && i18nUtil.doLeftAndRightSwapInRTL; | ||
| auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold. | ||
|
|
||
| _state->updateState( | ||
| [=](const ScrollContentViewShadowNode::ConcreteState::Data &oldData) | ||
| -> ScrollContentViewShadowNode::ConcreteState::SharedData { | ||
| auto deltaPadding = newPadding - oldData.safeAreaPadding; | ||
|
|
||
| if (std::abs(deltaPadding.left) < threshold && std::abs(deltaPadding.top) < threshold && | ||
| std::abs(deltaPadding.right) < threshold && std::abs(deltaPadding.bottom) < threshold && | ||
| oldData.layoutDirection == layoutDirection && oldData.swapLeftAndRightInRTL == swapLeftAndRightInRTL) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| auto newData = oldData; | ||
| newData.safeAreaPadding = newPadding; | ||
| newData.layoutDirection = layoutDirection; | ||
| newData.swapLeftAndRightInRTL = swapLeftAndRightInRTL; | ||
| return std::make_shared<const ScrollContentViewShadowNode::ConcreteState::Data>(newData); | ||
| }); | ||
| } | ||
|
|
||
| #pragma mark - RCTComponentViewProtocol | ||
|
|
||
| + (ComponentDescriptorProvider)componentDescriptorProvider | ||
| { | ||
| return concreteComponentDescriptorProvider<ScrollContentViewComponentDescriptor>(); | ||
| } | ||
|
|
||
| - (void)updateState:(const facebook::react::State::Shared &)state | ||
| oldState:(const facebook::react::State::Shared &)oldState | ||
| { | ||
| _state = std::static_pointer_cast<const ScrollContentViewShadowNode::ConcreteState>(state); | ||
| } | ||
|
|
||
| - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask | ||
| { | ||
| [super finalizeUpdates:updateMask]; | ||
| [self _updateStateIfNecessary]; | ||
| } | ||
|
|
||
| - (void)prepareForRecycle | ||
| { | ||
| [super prepareForRecycle]; | ||
| _state.reset(); | ||
| _safeAreaPadding = UIEdgeInsetsZero; | ||
| } | ||
|
|
||
| @end | ||
|
|
||
| Class<RCTComponentViewProtocol> RCTScrollContentViewCls(void) | ||
| { | ||
| return RCTScrollContentViewComponentView.class; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| #import "RCTCustomPullToRefreshViewProtocol.h" | ||
| #import "RCTEnhancedScrollView.h" | ||
| #import "RCTFabricComponentsPlugins.h" | ||
| #import "RCTScrollContentViewComponentView.h" | ||
| #import "RCTVirtualViewContainerState.h" | ||
|
|
||
| using namespace facebook::react; | ||
|
|
@@ -449,9 +450,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & | |
| } else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Always) { | ||
| scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways; | ||
| } | ||
| ((RCTEnhancedScrollView *)_scrollView).padsContentForSafeArea = | ||
| contentInsetAdjustmentBehavior != ContentInsetAdjustmentBehavior::Never; | ||
| _shouldUpdateContentInsetAdjustmentBehavior = NO; | ||
| } | ||
| scrollView.padsContentForSafeArea = newScrollViewProps.padsContentForSafeArea; | ||
|
|
||
| MAP_SCROLL_VIEW_PROP(disableIntervalMomentum); | ||
| MAP_SCROLL_VIEW_PROP(snapToInterval); | ||
|
|
@@ -464,6 +466,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & | |
|
|
||
| // Must run after `super updateProps:` so it is based on the latest calculated view. | ||
| ((RCTEnhancedScrollView *)_scrollView).inverted = [self isInverted]; | ||
| [self _updateContentViewSafeAreaPadding]; | ||
| } | ||
|
|
||
| - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState | ||
|
|
@@ -563,6 +566,7 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone | |
| [_containerView insertSubview:childComponentView atIndex:index]; | ||
| if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) { | ||
| _contentView = childComponentView; | ||
| [self _updateContentViewSafeAreaPadding]; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -866,6 +870,46 @@ - (void)didMoveToWindow | |
| // The view is being removed, ensure that the scroll end event is dispatched | ||
| [self _handleScrollEndIfNeeded]; | ||
| } | ||
| [self _updateContentViewSafeAreaPadding]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first layout happens before the state has the insets, and |
||
| } | ||
|
|
||
| - (void)safeAreaInsetsDidChange | ||
| { | ||
| [super safeAreaInsetsDidChange]; | ||
| [self _updateContentViewSafeAreaPadding]; | ||
| } | ||
|
|
||
| - (void)layoutSubviews | ||
| { | ||
| [super layoutSubviews]; | ||
| // Moving or resizing the view changes its insets without `safeAreaInsetsDidChange` firing; that only reports | ||
| // window-level changes. | ||
| [self _updateContentViewSafeAreaPadding]; | ||
| } | ||
|
|
||
| /* | ||
| * Hands the content container the safe area to pad its content by: the pair of insets across the scrollable axis, | ||
| * unless `contentInsetAdjustmentBehavior` is `never`. Along the scrollable axis UIKit turns the safe area into content | ||
| * insets instead, so content scrolls under the system UI. | ||
| */ | ||
| - (void)_updateContentViewSafeAreaPadding | ||
| { | ||
| if (![_contentView isKindOfClass:[RCTScrollContentViewComponentView class]]) { | ||
| return; | ||
| } | ||
| const auto &props = static_cast<const ScrollViewProps &>(*_props); | ||
| UIEdgeInsets padding = UIEdgeInsetsZero; | ||
| if (props.contentInsetAdjustmentBehavior != ContentInsetAdjustmentBehavior::Never) { | ||
| UIEdgeInsets insets = self.safeAreaInsets; | ||
| if (props.horizontal) { | ||
| padding.top = insets.top; | ||
| padding.bottom = insets.bottom; | ||
| } else { | ||
| padding.left = insets.left; | ||
| padding.right = insets.right; | ||
| } | ||
| } | ||
| ((RCTScrollContentViewComponentView *)_contentView).safeAreaPadding = padding; | ||
| } | ||
|
|
||
| - (void)_handleScrollEndIfNeeded | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /* | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <react/cxxstableapi/PrivateGuard.h> | ||
|
|
||
| #include <react/renderer/components/scrollview/ScrollContentViewShadowNode.h> | ||
| #include <react/renderer/core/ConcreteComponentDescriptor.h> | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| /* | ||
| * Descriptor for the <ScrollContentView> component. | ||
| */ | ||
| class ScrollContentViewComponentDescriptor final : public ConcreteComponentDescriptor<ScrollContentViewShadowNode> { | ||
| public: | ||
| using ConcreteComponentDescriptor::ConcreteComponentDescriptor; | ||
|
|
||
| void adopt(ShadowNode &shadowNode) const override | ||
| { | ||
| static_cast<ScrollContentViewShadowNode &>(shadowNode).applySafeAreaPadding(); | ||
| ConcreteComponentDescriptor::adopt(shadowNode); | ||
| } | ||
| }; | ||
|
|
||
| } // namespace facebook::react |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /* | ||
| * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| #include "ScrollContentViewProps.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| ScrollContentViewProps::ScrollContentViewProps( | ||
| const PropsParserContext& context, | ||
| const ScrollContentViewProps& sourceProps, | ||
| const RawProps& rawProps) | ||
| : ViewProps(context, sourceProps, rawProps) {} | ||
|
|
||
| } // namespace facebook::react |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_updateContentViewSafeAreaPaddingruns on every layout pass, props update, mount and window change, andfinalizeUpdatescalls_updateStateIfNecessarytoo. Most of these schedule anupdateStatewhose callback ends up returningnullptr. Could we return early here when the rounded padding and direction match what we last sent, so we don't queue no-op state updates?