fix scrollToTop not working on certain tabs

This commit is contained in:
Oleksii Bulenok
2026-07-14 14:07:25 +02:00
parent 9b13d0debf
commit 95fef56be3
2 changed files with 93 additions and 0 deletions
@@ -1,3 +1,84 @@
diff --git a/ios/Fabric/RNCPagerViewComponentView.mm b/ios/Fabric/RNCPagerViewComponentView.mm
index 652a5c123100e7010011f07649517aa9e0cbc554..9e038c353298f33f4f0fdb9d01cec38841b7a5c9 100644
--- a/ios/Fabric/RNCPagerViewComponentView.mm
+++ b/ios/Fabric/RNCPagerViewComponentView.mm
@@ -90,6 +90,62 @@ - (void)willMoveToSuperview:(UIView *)newSuperview {
}
}
+/*
+ * UIKit resolves several behaviors (status-bar-tap scroll-to-top, safe area
+ * propagation, appearance callbacks) by walking parentViewController from a
+ * view's nearest view controller up to the window's root view controller.
+ * The Paper implementation embeds the UIPageViewController into that chain
+ * via reactAddControllerToClosestParent:, but this Fabric implementation
+ * leaves it orphaned (parentViewController == nil), which among other things
+ * makes UIKit ignore every scroll view rendered inside the pager when
+ * handling the status bar scroll-to-top tap. Attach the page view controller
+ * to the nearest ancestor view controller to restore parity with Paper.
+ */
+- (void)attachNativePageViewControllerToNearestParent {
+ if (_nativePageViewController == nil ||
+ _nativePageViewController.parentViewController != nil) {
+ return;
+ }
+ UIResponder *responder = self.nextResponder;
+ while (responder != nil && ![responder isKindOfClass:[UIViewController class]]) {
+ responder = responder.nextResponder;
+ }
+ UIViewController *parent = (UIViewController *)responder;
+ if (parent == nil) {
+ return;
+ }
+ [parent addChildViewController:_nativePageViewController];
+ [_nativePageViewController didMoveToParentViewController:parent];
+}
+
+- (void)detachNativePageViewControllerFromParent {
+ if (_nativePageViewController.parentViewController == nil) {
+ return;
+ }
+ [_nativePageViewController willMoveToParentViewController:nil];
+ [_nativePageViewController removeFromParentViewController];
+}
+
+- (void)didMoveToWindow {
+ [super didMoveToWindow];
+ if (self.window != nil) {
+ [self attachNativePageViewControllerToNearestParent];
+ }
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ /*
+ * On the first didMoveToWindow the ancestor view controller may not be
+ * wired up yet (see callstack/react-native-pager-view#1089 for the same
+ * timing issue in v8), so retry here; the attach is a cheap no-op once
+ * the controller has a parent.
+ */
+ if (self.window != nil) {
+ [self attachNativePageViewControllerToNearestParent];
+ }
+}
+
#pragma mark - React API
@@ -126,6 +182,13 @@ -(void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
-(void)prepareForRecycle {
[super prepareForRecycle];
+ /*
+ * Undo the child view controller relationship added in
+ * attachNativePageViewControllerToNearestParent, otherwise the parent
+ * view controller keeps the page view controller (and its subtree) alive
+ * after unmount.
+ */
+ [self detachNativePageViewControllerFromParent];
_nativePageViewController = nil;
_currentIndex = -1;
}
diff --git a/ios/RNCPagerView.m b/ios/RNCPagerView.m
index adfc7c6f2224b898a02319d352bb4fe11a18fd7e..939bb801c5b0ca6f93b77cb0507c19d137e08e77 100644
--- a/ios/RNCPagerView.m
@@ -9,3 +9,15 @@ This patch adds the same logic for iOS 26's native `interactiveContentPopGesture
Related issues:
- https://github.com/software-mansion/react-native-screens/issues/3512
- https://github.com/software-mansion/react-native-screens/pull/3420
---
Also embeds the Fabric `UIPageViewController` into the view controller hierarchy (`ios/Fabric/RNCPagerViewComponentView.mm`).
The Paper implementation calls `reactAddControllerToClosestParent:` when embedding its `UIPageViewController`, so the controller becomes a child of the nearest ancestor view controller (e.g. `RNSScreen`). The Fabric implementation never does this - the page view controller is orphaned (`parentViewController == nil`).
UIKit resolves the status-bar-tap scroll-to-top gesture by walking `parentViewController`/`presentingViewController` from each candidate scroll view's nearest view controller up to the window's root (see `-[UIWindow _scrollToTopViewsUnderScreenPointIfNecessary:resultHandler:]`). With the orphaned controller that walk dead-ends, so every scroll view rendered inside a pager (all Home feeds, Profile tabs, etc.) is dropped from candidate selection and tapping the status bar no longer scrolls feeds to top. It only kept "working" when the window happened to contain exactly one other eligible scroll view, via UIKit's single-candidate fallback.
The patch attaches the page view controller to the nearest view controller found via the responder chain on `didMoveToWindow` (with a `layoutSubviews` retry because the ancestor controller may not be wired up on the first pass - same timing issue as callstack/react-native-pager-view#1089), and detaches it in `prepareForRecycle` to avoid leaking the controller after unmount.
Fixed upstream in v8 by the SwiftUI rewrite, which embeds via `reactViewController()` + `addChild` (see `PagerViewProvider.swift`).