Skip to content

Commit

Permalink
Add fabric support for maintainVisibleContentPosition on iOS (#36095)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
janicduplessis authored and facebook-github-bot committed Mar 7, 2023
1 parent a448c6d commit 59c4db8
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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>

/**
* Denotes a view which implements custom pull to refresh functionality.
*/
@protocol RCTCustomPullToRefreshViewProtocol

@end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#import <UIKit/UIKit.h>

#import <React/RCTCustomPullToRefreshViewProtocol.h>
#import <React/RCTViewComponentView.h>

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

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import <react/renderer/components/scrollview/conversions.h>

#import "RCTConversions.h"
#import "RCTCustomPullToRefreshViewProtocol.h"
#import "RCTEnhancedScrollView.h"
#import "RCTFabricComponentsPlugins.h"

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

CGPoint _contentOffsetWhenClipped;

__weak UIView *_contentView;

CGRect _prevFirstVisibleFrame;
__weak UIView *_firstVisibleView;
}

+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
Expand Down Expand Up @@ -148,10 +154,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 @@ -336,11 +349,18 @@ - (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_containerView insertSubview:childComponentView atIndex:index];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) {
_contentView = childComponentView;
}
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)] &&
_contentView == childComponentView) {
_contentView = nil;
}
}

/*
Expand Down Expand Up @@ -403,6 +423,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 @@ -683,6 +706,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 @@ -337,6 +346,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);
RAW_SET_PROP_SWITCH_CASE_BASIC(minimumZoomScale);
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollEnabled);
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,8 @@ 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
35 changes: 35 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 @@ -98,6 +99,26 @@ inline void fromRawValue(
abort();
}

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 ScrollViewSnapToAlignment &value) {
switch (value) {
case ScrollViewSnapToAlignment::Start:
Expand All @@ -109,6 +130,8 @@ inline std::string toString(const ScrollViewSnapToAlignment &value) {
}
}

#if RN_DEBUG_STRING_CONVERTIBLE

inline std::string toString(const ScrollViewIndicatorStyle &value) {
switch (value) {
case ScrollViewIndicatorStyle::Default:
Expand Down Expand Up @@ -144,5 +167,17 @@ inline std::string toString(const ContentInsetAdjustmentBehavior &value) {
}
}

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

#endif

} // namespace react
} // namespace facebook
17 changes: 17 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,8 @@

#pragma once

#include <optional>

namespace facebook {
namespace react {

Expand All @@ -23,5 +25,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
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class AppendingList extends React.Component<
<ScrollView
automaticallyAdjustContentInsets={false}
maintainVisibleContentPosition={{
minIndexForVisible: 1,
minIndexForVisible: 0,
autoscrollToTopThreshold: 10,
}}
nestedScrollEnabled
Expand Down

0 comments on commit 59c4db8

Please sign in to comment.