Skip to content

Commit

Permalink
Add fabric support for maintainVisibleContentPosition on iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
janicduplessis committed Nov 12, 2022
1 parent 8c2a4d0 commit 0e94c17
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#import "RCTConversions.h"
#import "RCTEnhancedScrollView.h"
#import "RCTFabricComponentsPlugins.h"
#import "RCTPullToRefreshViewComponentView.h"

using namespace facebook::react;

Expand Down Expand Up @@ -100,6 +101,11 @@ @implementation RCTScrollViewComponentView {
BOOL _shouldUpdateContentInsetAdjustmentBehavior;

CGPoint _contentOffsetWhenClipped;

__weak UIView *_contentView;

CGRect _prevFirstVisibleFrame;
__weak UIView *_firstVisibleView;
}

+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
Expand Down Expand Up @@ -149,10 +155,17 @@ - (void)dealloc

#pragma mark - RCTMountingTransactionObserving

- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
[self _prepareForMaintainVisibleScrollPosition];
}

- (void)mountingTransactionDidMount:(MountingTransaction const &)transaction
withSurfaceTelemetry:(facebook::react::SurfaceTelemetry const &)surfaceTelemetry
{
[self _remountChildren];
[self _adjustForMaintainVisibleContentPosition];
}

#pragma mark - RCTComponentViewProtocol
Expand Down Expand Up @@ -337,11 +350,23 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_containerView insertSubview:childComponentView atIndex:index];
if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) {
// Ignore the pull to refresh component.
} else {
RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview.");
_contentView = childComponentView;
}
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
if ([childComponentView isKindOfClass:RCTPullToRefreshViewComponentView.class]) {
// Ignore the pull to refresh component.
} else {
RCTAssert(_contentView == childComponentView, @"Attempted to remove non-existent subview");
_contentView = nil;
}
}

/*
Expand Down Expand Up @@ -404,6 +429,9 @@ - (void)prepareForRecycle
CGRect oldFrame = self.frame;
self.frame = CGRectZero;
self.frame = oldFrame;
_contentView = nil;
_prevFirstVisibleFrame = CGRectZero;
_firstVisibleView = nil;
[super prepareForRecycle];
}

Expand Down Expand Up @@ -684,6 +712,74 @@ - (void)removeScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
[self.scrollViewDelegateSplitter removeDelegate:scrollListener];
}

#pragma mark - Maintain visible content position

- (void)_prepareForMaintainVisibleScrollPosition
{
const auto &props = *std::static_pointer_cast<const ScrollViewProps>(_props);
if (!props.maintainVisibleContentPosition) {
return;
}

BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible;
for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) {
// Find the first entirely visible view.
UIView *subview = _contentView.subviews[ii];
BOOL hasNewView = NO;
if (horizontal) {
hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x;
} else {
hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y;
}
if (hasNewView || ii == _contentView.subviews.count - 1) {
_prevFirstVisibleFrame = subview.frame;
_firstVisibleView = subview;
break;
}
}
}

- (void)_adjustForMaintainVisibleContentPosition
{
const auto &props = *std::static_pointer_cast<const ScrollViewProps>(_props);
if (!props.maintainVisibleContentPosition) {
return;
}

std::optional<int> autoscrollThreshold = props.maintainVisibleContentPosition.value().autoscrollToTopThreshold;
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
// TODO: detect and handle/ignore re-ordering
if (horizontal) {
CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x;
if (ABS(deltaX) > 0.5) {
CGFloat x = _scrollView.contentOffset.x;
[self _forceDispatchNextScrollEvent];
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y);
if (autoscrollThreshold) {
// If the offset WAS within the threshold of the start, animate to the start.
if (x <= autoscrollThreshold.value()) {
[self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES];
}
}
}
} else {
CGRect newFrame = _firstVisibleView.frame;
CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y;
if (ABS(deltaY) > 0.5) {
CGFloat y = _scrollView.contentOffset.y;
[self _forceDispatchNextScrollEvent];
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY);
if (autoscrollThreshold) {
// If the offset WAS within the threshold of the start, animate to the start.
if (y <= autoscrollThreshold.value()) {
[self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES];
}
}
}
}
}

@end

Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ ScrollViewProps::ScrollViewProps(
"keyboardDismissMode",
sourceProps.keyboardDismissMode,
{})),
maintainVisibleContentPosition(
CoreFeatures::enablePropIteratorSetter
? sourceProps.maintainVisibleContentPosition
: convertRawProp(
context,
rawProps,
"maintainVisibleContentPosition",
sourceProps.maintainVisibleContentPosition,
{})),
maximumZoomScale(
CoreFeatures::enablePropIteratorSetter
? sourceProps.maximumZoomScale
Expand Down Expand Up @@ -336,6 +345,7 @@ void ScrollViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(directionalLockEnabled, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(indicatorStyle, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(keyboardDismissMode, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(maintainVisibleContentPosition, {});
RAW_SET_PROP_SWITCH_CASE_BASIC(maximumZoomScale, (Float)1.0);
RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale, (Float)1.0);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled, true);
Expand Down Expand Up @@ -413,6 +423,10 @@ SharedDebugStringConvertibleList ScrollViewProps::getDebugProps() const {
"keyboardDismissMode",
keyboardDismissMode,
defaultScrollViewProps.keyboardDismissMode),
debugStringConvertibleItem(
"maintainVisibleContentPosition",
maintainVisibleContentPosition,
defaultScrollViewProps.maintainVisibleContentPosition),
debugStringConvertibleItem(
"maximumZoomScale",
maximumZoomScale,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <react/renderer/components/view/ViewProps.h>
#include <react/renderer/core/PropsParserContext.h>

#include <optional>

namespace facebook {
namespace react {

Expand Down Expand Up @@ -43,6 +45,7 @@ class ScrollViewProps final : public ViewProps {
bool directionalLockEnabled{};
ScrollViewIndicatorStyle indicatorStyle{};
ScrollViewKeyboardDismissMode keyboardDismissMode{};
std::optional<ScrollViewMaintainVisibleContentPosition> maintainVisibleContentPosition{};
Float maximumZoomScale{1.0f};
Float minimumZoomScale{1.0f};
bool scrollEnabled{true};
Expand Down
31 changes: 31 additions & 0 deletions ReactCommon/react/renderer/components/scrollview/conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <folly/dynamic.h>
#include <react/renderer/components/scrollview/primitives.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/propsConversions.h>

namespace facebook {
namespace react {
Expand Down Expand Up @@ -144,5 +145,35 @@ inline std::string toString(const ContentInsetAdjustmentBehavior &value) {
}
}

inline void fromRawValue(
const PropsParserContext &context,
const RawValue &value,
ScrollViewMaintainVisibleContentPosition &result) {
auto map = (butter::map<std::string, RawValue>)value;

auto minIndexForVisible = map.find("minIndexForVisible");
if (minIndexForVisible != map.end()) {
fromRawValue(
context, minIndexForVisible->second, result.minIndexForVisible);
}
auto autoscrollToTopThreshold = map.find("autoscrollToTopThreshold");
if (autoscrollToTopThreshold != map.end()) {
fromRawValue(
context,
autoscrollToTopThreshold->second,
result.autoscrollToTopThreshold);
}
}

inline std::string toString(
const std::optional<ScrollViewMaintainVisibleContentPosition> &value) {
if (!value) {
return "null";
}
return "{minIndexForVisible: " + toString(value.value().minIndexForVisible) +
", autoscrollToTopThreshold: " +
toString(value.value().autoscrollToTopThreshold) + "}";
}

} // namespace react
} // namespace facebook
18 changes: 18 additions & 0 deletions ReactCommon/react/renderer/components/scrollview/primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

#pragma once

#include <optional>
#include <vector>

namespace facebook {
namespace react {

Expand All @@ -23,5 +26,20 @@ enum class ContentInsetAdjustmentBehavior {
Always
};

class ScrollViewMaintainVisibleContentPosition final {
public:
int minIndexForVisible{0};
std::optional<int> autoscrollToTopThreshold{};

bool operator==(const ScrollViewMaintainVisibleContentPosition &rhs) const {
return std::tie(this->minIndexForVisible, this->autoscrollToTopThreshold) ==
std::tie(rhs.minIndexForVisible, rhs.autoscrollToTopThreshold);
}

bool operator!=(const ScrollViewMaintainVisibleContentPosition &rhs) const {
return !(*this == rhs);
}
};

} // namespace react
} // namespace facebook
9 changes: 9 additions & 0 deletions ReactCommon/react/renderer/debug/DebugStringConvertible.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <climits>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
Expand Down Expand Up @@ -98,6 +99,14 @@ std::string toString(float const &value);
std::string toString(double const &value);
std::string toString(void const *value);

template <typename T>
std::string toString(const std::optional<T> &value) {
if (!value) {
return "null";
}
return toString(value.value());
}

/*
* *Informal* `DebugStringConvertible` interface.
*
Expand Down
78 changes: 39 additions & 39 deletions packages/rn-tester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -938,11 +938,11 @@ EXTERNAL SOURCES:
:path: "../../ReactCommon/yoga"

SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: d68947eddece25638eb0f642d1b957c90388afd1
FBReactNativeSpec: 9a029e7dec747a8836d785b3b7a433db5960504b
FBLazyVector: d1314b8103bbf3209d50ba8dfba22d650e46030c
FBReactNativeSpec: 938f038a9a99787d4e2696564e39c69b4f880b95
Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30
Expand All @@ -954,48 +954,48 @@ SPEC CHECKSUMS:
FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: d344c89c3f4657f7031e5280e1b3dd531b425bfd
hermes-engine: 4c0c4d6c03ed75f7aedf2a6537918b34c68f27f6
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: 54a4f03dbbebb0cfdb4e2ba8d3b1d0b1258f8c08
RCTTypeSafety: a41e253b4ed644708899857d912b2f50c7b6214d
React: 2fc6c4c656cccd6753016528ad41199c16fd558e
React-callinvoker: a7d5e883a83bb9bd3985b08be832c5e76451d18f
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: abf913aae52045e6ac052b10e2984593b9e4c7d9
RCTTypeSafety: db37cf760c05619eaa7f18b4f064d57d8afc19c6
React: 512bd9eb992cf723bedece290520b6825dc3177c
React-callinvoker: 6e31d603d4bb3643819ed03b7599d395d986aedc
React-Codegen: 4a022870a58b95e17da8f32641ba3d72b551f268
React-Core: 719bec4b41c93b1affb1e2c3a43956ec482ecb9f
React-CoreModules: feaa45c54c58e1420981f6dd544c8b3d01200caa
React-cxxreact: c5f93e7a35f3545489d8e1f89beb9d2d56acfde5
React-Fabric: 8a854fd89c932ab073f67036bb45d1787d0d31a4
React-graphics: cb8a85648695c60f33a00d732b985f734d1470d8
React-hermes: 299c7f56d32e8953480fd8e7fba2a7968a534b3f
React-jsi: d40e13b7f545f9af2af780f153f5321018b5e2f8
React-jsidynamic: 8aa406dfc1eff081f3443e55a28b51d11616a3bf
React-jsiexecutor: 04a945f040cc085d79655359ec29e5f501fb6e01
React-jsinspector: a56861590ddfcb5cb544877ade3e007a32ff9616
React-logger: 07c9b44040a6f948b8e2033207b23cb623f0b9b4
React-perflogger: b4b9fb2ddd856b78003708ab3cf66ce03e6bc7c4
React-RCTActionSheet: 1b1501ef80928be10702cd0ce09120358094cd82
React-RCTAnimation: 6741f7be3e269e057c1426074cc70f34b56e114b
React-RCTAppDelegate: 0b3b2c1e02c02f952f5033535ddb23d690e3b890
React-RCTBlob: 94feb99abafd0527a78f6caaa17a0bcec9ce3167
React-RCTFabric: db1d7fe55db4811b63ae4060078e7048ebb4a918
React-RCTImage: 055685a12c88939437f6520d9e7c120cd666cbf1
React-RCTLinking: b149b3ff1f96fa93fc445230b9c171adb0e5572c
React-RCTNetwork: 21abb4231182651f48b7035beaa011b1ab7ae8f4
React-RCTPushNotification: f3af966de34c1fe2df8860625d225fb2f581d15e
React-RCTSettings: 64b6acabfddf7f96796229b101bd91f46d14391b
React-RCTTest: 81ebfa8c2e1b0b482effe12485e6486dc0ff70d7
React-RCTText: 4e5ae05b778a0ed2b22b012af025da5e1a1c4e54
React-RCTVibration: ecfd04c1886a9c9a4e31a466c0fbcf6b36e92fde
React-rncore: 08566b41339706758229f407c8907b2f7987f058
React-runtimeexecutor: c7b2cd6babf6cc50340398bfbb7a9da13c93093f
ReactCommon: fc336a81ae40421e172c3ca9496677e34d7e3ed5
React-Core: 5242baffa4b9742500d19dcd398dba441e8aeda7
React-CoreModules: 38c9ae7f78e033c3875ba25d8ae2d39d844f828f
React-cxxreact: 2a785babb607be7e407309c7f124cee9d3b921b4
React-Fabric: 5d7f6967018a6617438af98c389a4b4a2af7efc7
React-graphics: b9f9cce87199f460270210c30d3cc3b5ae6fdf35
React-hermes: 42092890a4d49b8fa579ade31a0ce69e6abc806c
React-jsi: 83acdf27e0d369569e431c245e6238e25bb6b338
React-jsidynamic: 8383c0e355a3e72d36b72c70f8f39e920906a766
React-jsiexecutor: 0a02e213d7e42fa7686639eb30b9c1c9b9ae33bf
React-jsinspector: 5d837065f3d3fb5cb72060371e669c3fd380471c
React-logger: 18e585e68be93ed6e630aedcb1a1dfa2ade1723f
React-perflogger: c5e84d3019949a31374fbc279915123c028e24e8
React-RCTActionSheet: 4facc4e45a13e7f2551b3264a662b5d1104ca926
React-RCTAnimation: 0865abae40177342efeb4065c53f052e06c03689
React-RCTAppDelegate: 738bf72afc34a066964ac73c886402bc34047342
React-RCTBlob: ff08ae9b3254a491145501bb247d3fef124b89ce
React-RCTFabric: bfac03a1533e117b53cfdb2401e0788ba15278fa
React-RCTImage: 37c72ad7673b500504793364d6186d880fbfd17c
React-RCTLinking: 68a464d8166a31b59ef3a218192b4f64d826c19b
React-RCTNetwork: 89e08588d5faf51d3d797fe11f36b8c03a831156
React-RCTPushNotification: 68743863feb124d99c86e4b605d5c01d17ea1cce
React-RCTSettings: e5fad8931f831e2274c2422af671cd827756fb4e
React-RCTTest: a62120cd93d3df322eef48af9fae06fd730ce93c
React-RCTText: e34b1671bcedb3988aba964a1851cab222377161
React-RCTVibration: f2567d752318e6fd1c838b435c254435a487852f
React-rncore: 714be3b4bc3834753121b42c1cac66e9c6012efa
React-runtimeexecutor: 3b7744e88c90f98f8ba81d0a7af0088a5562b829
ReactCommon: aee00da9e33f228efaf3005589ebfa528b221267
ScreenshotManager: 2bd28f9b590a13c811f1f4ce32aab767f8845c6b
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 1b1a12ff3d86a10565ea7cbe057d42f5e5fb2a07
Yoga: e9cb8aab0a96ae9d6bcbb5dc4fb74761171e9f44
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: 20298ecd3f30aa788ad491637e593ed0d8c100ca
PODFILE CHECKSUM: 5d1fc1e8809808c4384337ae55c5be2de48ffe4c

COCOAPODS: 1.11.3
Loading

0 comments on commit 0e94c17

Please sign in to comment.