Skip to content

Commit 59c4db8

Browse files
janicduplessisfacebook-github-bot
authored andcommitted
Add fabric support for maintainVisibleContentPosition on iOS (#36095)
Summary: Reland of #35319 with a fix for custom pull to refresh components. Custom pull to refresh component in fabric will need to conform to the `RCTCustomPullToRefreshViewProtocol` protocol, this way we know that the view is a pull to refresh and not the content view. ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [IOS] [ADDED] - Add fabric support for maintainVisibleContentPosition on iOS For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> Pull Request resolved: #36095 Test Plan: This will need to be tested internally in the product the crash happened. Take a local build of Wilde open Marketplace. Reviewed By: jacdebug Differential Revision: D43128163 Pulled By: cipolleschi fbshipit-source-id: 6cf8ddff92aeb446072a3d847434e21b9e38af61
1 parent a448c6d commit 59c4db8

9 files changed

Lines changed: 188 additions & 2 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <UIKit/UIKit.h>
9+
10+
/**
11+
* Denotes a view which implements custom pull to refresh functionality.
12+
*/
13+
@protocol RCTCustomPullToRefreshViewProtocol
14+
15+
@end

React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#import <UIKit/UIKit.h>
99

10+
#import <React/RCTCustomPullToRefreshViewProtocol.h>
1011
#import <React/RCTViewComponentView.h>
1112

1213
NS_ASSUME_NONNULL_BEGIN
@@ -16,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
1617
* This view is designed to only serve ViewController-like purpose for the actual `UIRefreshControl` view which is being
1718
* attached to some `UIScrollView` (not to this view).
1819
*/
19-
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView
20+
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
2021

2122
@end
2223

React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#import <react/renderer/components/scrollview/conversions.h>
2121

2222
#import "RCTConversions.h"
23+
#import "RCTCustomPullToRefreshViewProtocol.h"
2324
#import "RCTEnhancedScrollView.h"
2425
#import "RCTFabricComponentsPlugins.h"
2526

@@ -99,6 +100,11 @@ @implementation RCTScrollViewComponentView {
99100
BOOL _shouldUpdateContentInsetAdjustmentBehavior;
100101

101102
CGPoint _contentOffsetWhenClipped;
103+
104+
__weak UIView *_contentView;
105+
106+
CGRect _prevFirstVisibleFrame;
107+
__weak UIView *_firstVisibleView;
102108
}
103109

104110
+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
@@ -148,10 +154,17 @@ - (void)dealloc
148154

149155
#pragma mark - RCTMountingTransactionObserving
150156

157+
- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
158+
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
159+
{
160+
[self _prepareForMaintainVisibleScrollPosition];
161+
}
162+
151163
- (void)mountingTransactionDidMount:(MountingTransaction const &)transaction
152164
withSurfaceTelemetry:(facebook::react::SurfaceTelemetry const &)surfaceTelemetry
153165
{
154166
[self _remountChildren];
167+
[self _adjustForMaintainVisibleContentPosition];
155168
}
156169

157170
#pragma mark - RCTComponentViewProtocol
@@ -336,11 +349,18 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block
336349
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
337350
{
338351
[_containerView insertSubview:childComponentView atIndex:index];
352+
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) {
353+
_contentView = childComponentView;
354+
}
339355
}
340356

341357
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
342358
{
343359
[childComponentView removeFromSuperview];
360+
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)] &&
361+
_contentView == childComponentView) {
362+
_contentView = nil;
363+
}
344364
}
345365

346366
/*
@@ -403,6 +423,9 @@ - (void)prepareForRecycle
403423
CGRect oldFrame = self.frame;
404424
self.frame = CGRectZero;
405425
self.frame = oldFrame;
426+
_contentView = nil;
427+
_prevFirstVisibleFrame = CGRectZero;
428+
_firstVisibleView = nil;
406429
[super prepareForRecycle];
407430
}
408431

@@ -683,6 +706,74 @@ - (void)removeScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
683706
[self.scrollViewDelegateSplitter removeDelegate:scrollListener];
684707
}
685708

709+
#pragma mark - Maintain visible content position
710+
711+
- (void)_prepareForMaintainVisibleScrollPosition
712+
{
713+
const auto &props = *std::static_pointer_cast<const ScrollViewProps>(_props);
714+
if (!props.maintainVisibleContentPosition) {
715+
return;
716+
}
717+
718+
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
719+
int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible;
720+
for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) {
721+
// Find the first entirely visible view.
722+
UIView *subview = _contentView.subviews[ii];
723+
BOOL hasNewView = NO;
724+
if (horizontal) {
725+
hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x;
726+
} else {
727+
hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y;
728+
}
729+
if (hasNewView || ii == _contentView.subviews.count - 1) {
730+
_prevFirstVisibleFrame = subview.frame;
731+
_firstVisibleView = subview;
732+
break;
733+
}
734+
}
735+
}
736+
737+
- (void)_adjustForMaintainVisibleContentPosition
738+
{
739+
const auto &props = *std::static_pointer_cast<const ScrollViewProps>(_props);
740+
if (!props.maintainVisibleContentPosition) {
741+
return;
742+
}
743+
744+
std::optional<int> autoscrollThreshold = props.maintainVisibleContentPosition.value().autoscrollToTopThreshold;
745+
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
746+
// TODO: detect and handle/ignore re-ordering
747+
if (horizontal) {
748+
CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x;
749+
if (ABS(deltaX) > 0.5) {
750+
CGFloat x = _scrollView.contentOffset.x;
751+
[self _forceDispatchNextScrollEvent];
752+
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y);
753+
if (autoscrollThreshold) {
754+
// If the offset WAS within the threshold of the start, animate to the start.
755+
if (x <= autoscrollThreshold.value()) {
756+
[self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES];
757+
}
758+
}
759+
}
760+
} else {
761+
CGRect newFrame = _firstVisibleView.frame;
762+
CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y;
763+
if (ABS(deltaY) > 0.5) {
764+
CGFloat y = _scrollView.contentOffset.y;
765+
[self _forceDispatchNextScrollEvent];
766+
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY);
767+
if (autoscrollThreshold) {
768+
// If the offset WAS within the threshold of the start, animate to the start.
769+
if (y <= autoscrollThreshold.value()) {
770+
[self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES];
771+
}
772+
}
773+
}
774+
}
775+
}
776+
686777
@end
687778

688779
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)

ReactCommon/react/renderer/components/scrollview/ScrollViewProps.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ ScrollViewProps::ScrollViewProps(
127127
"keyboardDismissMode",
128128
sourceProps.keyboardDismissMode,
129129
{})),
130+
maintainVisibleContentPosition(
131+
CoreFeatures::enablePropIteratorSetter
132+
? sourceProps.maintainVisibleContentPosition
133+
: convertRawProp(
134+
context,
135+
rawProps,
136+
"maintainVisibleContentPosition",
137+
sourceProps.maintainVisibleContentPosition,
138+
{})),
130139
maximumZoomScale(
131140
CoreFeatures::enablePropIteratorSetter
132141
? sourceProps.maximumZoomScale
@@ -337,6 +346,7 @@ void ScrollViewProps::setProp(
337346
RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled);
338347
RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle);
339348
RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode);
349+
RAW_SET_PROP_SWITCH_CASE_BASIC(maintainVisibleContentPosition);
340350
RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale);
341351
RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale);
342352
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled);
@@ -413,6 +423,10 @@ SharedDebugStringConvertibleList ScrollViewProps::getDebugProps() const {
413423
"keyboardDismissMode",
414424
keyboardDismissMode,
415425
defaultScrollViewProps.keyboardDismissMode),
426+
debugStringConvertibleItem(
427+
"maintainVisibleContentPosition",
428+
maintainVisibleContentPosition,
429+
defaultScrollViewProps.maintainVisibleContentPosition),
416430
debugStringConvertibleItem(
417431
"maximumZoomScale",
418432
maximumZoomScale,

ReactCommon/react/renderer/components/scrollview/ScrollViewProps.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <react/renderer/components/view/ViewProps.h>
1212
#include <react/renderer/core/PropsParserContext.h>
1313

14+
#include <optional>
15+
1416
namespace facebook {
1517
namespace react {
1618

@@ -43,6 +45,8 @@ class ScrollViewProps final : public ViewProps {
4345
bool directionalLockEnabled{};
4446
ScrollViewIndicatorStyle indicatorStyle{};
4547
ScrollViewKeyboardDismissMode keyboardDismissMode{};
48+
std::optional<ScrollViewMaintainVisibleContentPosition>
49+
maintainVisibleContentPosition{};
4650
Float maximumZoomScale{1.0f};
4751
Float minimumZoomScale{1.0f};
4852
bool scrollEnabled{true};

ReactCommon/react/renderer/components/scrollview/conversions.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <folly/dynamic.h>
1111
#include <react/renderer/components/scrollview/primitives.h>
1212
#include <react/renderer/core/PropsParserContext.h>
13+
#include <react/renderer/core/propsConversions.h>
1314

1415
namespace facebook {
1516
namespace react {
@@ -98,6 +99,26 @@ inline void fromRawValue(
9899
abort();
99100
}
100101

102+
inline void fromRawValue(
103+
const PropsParserContext &context,
104+
const RawValue &value,
105+
ScrollViewMaintainVisibleContentPosition &result) {
106+
auto map = (butter::map<std::string, RawValue>)value;
107+
108+
auto minIndexForVisible = map.find("minIndexForVisible");
109+
if (minIndexForVisible != map.end()) {
110+
fromRawValue(
111+
context, minIndexForVisible->second, result.minIndexForVisible);
112+
}
113+
auto autoscrollToTopThreshold = map.find("autoscrollToTopThreshold");
114+
if (autoscrollToTopThreshold != map.end()) {
115+
fromRawValue(
116+
context,
117+
autoscrollToTopThreshold->second,
118+
result.autoscrollToTopThreshold);
119+
}
120+
}
121+
101122
inline std::string toString(const ScrollViewSnapToAlignment &value) {
102123
switch (value) {
103124
case ScrollViewSnapToAlignment::Start:
@@ -109,6 +130,8 @@ inline std::string toString(const ScrollViewSnapToAlignment &value) {
109130
}
110131
}
111132

133+
#if RN_DEBUG_STRING_CONVERTIBLE
134+
112135
inline std::string toString(const ScrollViewIndicatorStyle &value) {
113136
switch (value) {
114137
case ScrollViewIndicatorStyle::Default:
@@ -144,5 +167,17 @@ inline std::string toString(const ContentInsetAdjustmentBehavior &value) {
144167
}
145168
}
146169

170+
inline std::string toString(
171+
const std::optional<ScrollViewMaintainVisibleContentPosition> &value) {
172+
if (!value) {
173+
return "null";
174+
}
175+
return "{minIndexForVisible: " + toString(value.value().minIndexForVisible) +
176+
", autoscrollToTopThreshold: " +
177+
toString(value.value().autoscrollToTopThreshold) + "}";
178+
}
179+
180+
#endif
181+
147182
} // namespace react
148183
} // namespace facebook

ReactCommon/react/renderer/components/scrollview/primitives.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#pragma once
99

10+
#include <optional>
11+
1012
namespace facebook {
1113
namespace react {
1214

@@ -23,5 +25,20 @@ enum class ContentInsetAdjustmentBehavior {
2325
Always
2426
};
2527

28+
class ScrollViewMaintainVisibleContentPosition final {
29+
public:
30+
int minIndexForVisible{0};
31+
std::optional<int> autoscrollToTopThreshold{};
32+
33+
bool operator==(const ScrollViewMaintainVisibleContentPosition &rhs) const {
34+
return std::tie(this->minIndexForVisible, this->autoscrollToTopThreshold) ==
35+
std::tie(rhs.minIndexForVisible, rhs.autoscrollToTopThreshold);
36+
}
37+
38+
bool operator!=(const ScrollViewMaintainVisibleContentPosition &rhs) const {
39+
return !(*this == rhs);
40+
}
41+
};
42+
2643
} // namespace react
2744
} // namespace facebook

ReactCommon/react/renderer/debug/DebugStringConvertible.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <climits>
1111
#include <memory>
12+
#include <optional>
1213
#include <string>
1314
#include <unordered_set>
1415
#include <vector>
@@ -98,6 +99,14 @@ std::string toString(float const &value);
9899
std::string toString(double const &value);
99100
std::string toString(void const *value);
100101

102+
template <typename T>
103+
std::string toString(const std::optional<T> &value) {
104+
if (!value) {
105+
return "null";
106+
}
107+
return toString(value.value());
108+
}
109+
101110
/*
102111
* *Informal* `DebugStringConvertible` interface.
103112
*

packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class AppendingList extends React.Component<
7676
<ScrollView
7777
automaticallyAdjustContentInsets={false}
7878
maintainVisibleContentPosition={{
79-
minIndexForVisible: 1,
79+
minIndexForVisible: 0,
8080
autoscrollToTopThreshold: 10,
8181
}}
8282
nestedScrollEnabled

0 commit comments

Comments
 (0)