Skip to content

fix(ios): yield to the navigation back gesture on the first page - #1195

Open
abulenok wants to merge 3 commits into
callstack:mainfrom
abulenok:fix/ios-yield-to-back-gesture
Open

abulenok wants to merge 3 commits into
callstack:mainfrom
abulenok:fix/ios-yield-to-back-gesture

Conversation

@abulenok

@abulenok abulenok commented Oct 20, 2026 •

Copy link
Copy Markdown

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

OS Implemented
iOS ✅
Android ❌

Checklist

  • I have tested this on a device and a simulator
  • I added the documentation in README.md
  • I updated the typed files (TS and Flow)

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread ios/PagerGestureObserver.swift Outdated
Comment thread ios/PagerView.swift

@Doberjohn Doberjohn left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Add "PagerGestureObserver.swift" to the RNCPagerViewSwift sources list.
  2. Add the import the other Swift files use, since PagerLayoutDirection now comes from the ObjC module under SPM:
#if SWIFT_PACKAGE
import react_native_pager_view
#endif

The merge is clean and ios-spm isn't a required check, so nothing would catch this before it lands.

@Doberjohn

Copy link
Copy Markdown

Could you add an e2e test and fill in the Test Plan? The fix depends on private class names (UIScrollViewPagingSwipeGestureRecognizer, the RNS recognizers), on when introspect calls customize, and on gesture ordering, so a later change could quietly turn it into a no-op. The example already pushes every screen on a native stack, so an e2e/tests/issues/issue-1050-*.e2e.ts should be small:

  • page 0: a mid-screen back swipe pops the screen
  • page 1: the same swipe goes to page 0 and does not pop
  • an RTL variant
  • a forward swipe on page 0 still reaches page 1

Comment on lines +74 to +85
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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
}

@troZee
troZee requested a balanced review from Copilot October 22, 2026 17:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] react-native-screens v4.19.0 breaks the global back swipe gesture on pager views

3 participants