Skip to content
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

[iOS] fix: incorrect ScrollView offset on update #30647

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion React/Views/ScrollView/RCTScrollContentView.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ - (void)reactSetFrame:(CGRect)frame

RCTAssert([scrollView isKindOfClass:[RCTScrollView class]], @"Unexpected view hierarchy of RCTScrollView component.");

[scrollView updateContentOffsetIfNeeded];
[scrollView updateContentSizeIfNeeded];
}

@end
8 changes: 1 addition & 7 deletions React/Views/ScrollView/RCTScrollView.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@
*/
@property (nonatomic, readonly) UIView *contentView;

/**
* If the `contentSize` is not specified (or is specified as {0, 0}, then the
* `contentSize` will automatically be determined by the size of the subview.
*/
@property (nonatomic, assign) CGSize contentSize;

/**
* The underlying scrollView (TODO: can we remove this?)
*/
Expand Down Expand Up @@ -68,7 +62,7 @@

@interface RCTScrollView (Internal)

- (void)updateContentOffsetIfNeeded;
- (void)updateContentSizeIfNeeded;

@end

Expand Down
60 changes: 2 additions & 58 deletions React/Views/ScrollView/RCTScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ - (instancetype)initWithEventDispatcher:(id<RCTEventDispatcherProtocol>)eventDis

_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
_contentSize = CGSizeZero;
_lastClippedToRect = CGRectNull;

_scrollEventThrottle = 0.0;
Expand Down Expand Up @@ -381,7 +380,7 @@ - (void)didUpdateReactSubviews
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
if ([changedProps containsObject:@"contentSize"]) {
[self updateContentOffsetIfNeeded];
[self updateContentSizeIfNeeded];
}
}

Expand Down Expand Up @@ -813,71 +812,16 @@ - (CGSize)_calculateViewportSize
return viewportSize;
}

- (CGPoint)calculateOffsetForContentSize:(CGSize)newContentSize
{
CGPoint oldOffset = _scrollView.contentOffset;
CGPoint newOffset = oldOffset;

CGSize oldContentSize = _scrollView.contentSize;
CGSize viewportSize = [self _calculateViewportSize];

BOOL fitsinViewportY = oldContentSize.height <= viewportSize.height && newContentSize.height <= viewportSize.height;
if (newContentSize.height < oldContentSize.height && !fitsinViewportY) {
CGFloat offsetHeight = oldOffset.y + viewportSize.height;
if (oldOffset.y < 0) {
// overscrolled on top, leave offset alone
} else if (offsetHeight > oldContentSize.height) {
// overscrolled on the bottom, preserve overscroll amount
newOffset.y = MAX(0, oldOffset.y - (oldContentSize.height - newContentSize.height));
} else if (offsetHeight > newContentSize.height) {
// offset falls outside of bounds, scroll back to end of list
newOffset.y = MAX(0, newContentSize.height - viewportSize.height);
}
}

BOOL fitsinViewportX = oldContentSize.width <= viewportSize.width && newContentSize.width <= viewportSize.width;
if (newContentSize.width < oldContentSize.width && !fitsinViewportX) {
CGFloat offsetHeight = oldOffset.x + viewportSize.width;
if (oldOffset.x < 0) {
// overscrolled at the beginning, leave offset alone
} else if (offsetHeight > oldContentSize.width && newContentSize.width > viewportSize.width) {
// overscrolled at the end, preserve overscroll amount as much as possible
newOffset.x = MAX(0, oldOffset.x - (oldContentSize.width - newContentSize.width));
} else if (offsetHeight > newContentSize.width) {
// offset falls outside of bounds, scroll back to end
newOffset.x = MAX(0, newContentSize.width - viewportSize.width);
}
}

// all other cases, offset doesn't change
return newOffset;
}

/**
* Once you set the `contentSize`, to a nonzero value, it is assumed to be
* managed by you, and we'll never automatically compute the size for you,
* unless you manually reset it back to {0, 0}
*/
- (CGSize)contentSize
{
if (!CGSizeEqualToSize(_contentSize, CGSizeZero)) {
return _contentSize;
}

return _contentView.frame.size;
}

- (void)updateContentOffsetIfNeeded
- (void)updateContentSizeIfNeeded
{
CGSize contentSize = self.contentSize;
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
// When contentSize is set manually, ScrollView internals will reset
// contentOffset to {0, 0}. Since we potentially set contentSize whenever
// anything in the ScrollView updates, we workaround this issue by manually
// adjusting contentOffset whenever this happens
CGPoint newOffset = [self calculateOffsetForContentSize:contentSize];
_scrollView.contentSize = contentSize;
_scrollView.contentOffset = newOffset;
}
}

Expand Down