Repository navigation
Conversation
The SwiftUI pager's collection view claimed every horizontal drag, so react-native-screens' full-screen back swipe (its own pan recognizer before iOS 26, interactiveContentPopGestureRecognizer on iOS 26) never won, even on the first page. Recognizing simultaneously does not help because UIKit still hands the touch to the scroll view, so the pan has to be refused before it begins. UIKit only asks the scroll view itself in gestureRecognizerShouldBegin, so the introspected collection view gets a runtime subclass, the same approach disableSafeArea() uses. The pan is refused only on page 0 of a horizontal pager, only in the back direction, and only while the nearest navigation controller can pop. iOS 26 also attaches UIScrollViewPagingSwipeGestureRecognizer to paging scroll views, which flips a page on a quick flick even when the pan never begins; it is refused under the same conditions. Fixes callstack#1050 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Arbitration misses iOS 15’s embedded paging scroller and suppresses overdrag when no eligible back gesture is available.
2 open issues
What changed in this PR
Updates the SwiftUI iOS pager to yield first-page back swipes to navigation, addressing #1050.
Changes:
- Tracks pager state for gesture arbitration.
- Adds runtime overrides for paging pans and iOS 26 paging-swipe recognizers.
| File | Description |
|---|---|
| ios/PagerView.swift | Attaches the observer and synchronizes pager state. |
| ios/PagerGestureObserver.swift | Implements direction-aware back-gesture arbitration. |
Review effort: Balanced
Doberjohn
left a comment
There was a problem hiding this comment.
This needs a rebase onto main. #1187 added SwiftPM support, and ios/Package.swift lists its Swift sources by name, so after the merge PagerGestureObserver.swift isn't compiled under SPM and PagerView.swift fails with "cannot find PagerGestureObserver in scope". Two changes fix it:
- Add
"PagerGestureObserver.swift"to theRNCPagerViewSwiftsourceslist. - Add the import the other Swift files use, since
PagerLayoutDirectionnow comes from the ObjC module under SPM:
#if SWIFT_PACKAGE
import react_native_pager_view
#endifThe merge is clean and ios-spm isn't a required check, so nothing would catch this before it lands.
|
Could you add an e2e test and fill in the Test Plan? The fix depends on private class names (
|
| while let current = responder { | ||
| if let found = current as? UINavigationController { | ||
| navigationController = found | ||
| break | ||
| } | ||
|
|
||
| responder = current.next | ||
| } | ||
|
|
||
| guard let navigationController, navigationController.viewControllers.count > 1 else { | ||
| return false | ||
| } |
There was a problem hiding this comment.
This stops at the nearest UINavigationController and returns false when it has one view controller. A pager on the root screen of a nested stack therefore never yields, even when the outer stack can pop (common with nested navigators in Expo Router and react-navigation). Skipping stacks that can't pop fixes it:
| while let current = responder { | |
| if let found = current as? UINavigationController { | |
| navigationController = found | |
| break | |
| } | |
| responder = current.next | |
| } | |
| guard let navigationController, navigationController.viewControllers.count > 1 else { | |
| return false | |
| } | |
| while let current = responder { | |
| if let found = current as? UINavigationController, found.viewControllers.count > 1 { | |
| navigationController = found | |
| break | |
| } | |
| responder = current.next | |
| } | |
| guard let navigationController else { | |
| return false | |
| } |
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

Summary
The SwiftUI pager's collection view claimed every horizontal drag, so react-native-screens' full-screen back swipe (its own pan recognizer before iOS 26, interactiveContentPopGestureRecognizer on iOS 26) never won, even on the first page. Recognizing simultaneously does not help because UIKit still hands the touch to the scroll view, so the pan has to be refused before it begins. UIKit only asks the scroll view itself in gestureRecognizerShouldBegin, so the introspected collection view gets a runtime subclass, the same approach disableSafeArea() uses.
The pan is refused only on page 0 of a horizontal pager, only in the back direction, and only while the nearest navigation controller can pop. iOS 26 also attaches UIScrollViewPagingSwipeGestureRecognizer to paging scroll views, which flips a page on a quick flick even when the pan never begins; it is refused under the same conditions.
Fixes #1050
The implementation is based on @nickbvr comment #1050 (comment)
Test Plan
What's required for testing (prerequisites)?
What are the steps to reproduce (after prerequisites)?
Compatibility
Checklist
README.md