authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-03 15:59:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 15:59:27-07:00
log80b53730d1e821563e5c348fdf4f5fc7616a49f7
tree8f3590ed8a4f09e2ec9200742a7cd4691d5f1c18
parent409bc4fa6f3bb9869052bdba4244b30606457f20
signature Signed by PGP key B5690EEEBB952194

fix(mobile): let back swipe pop from horizontal scroll edges (#9493)

Co-authored-by: Claude Code <noreply@anthropic.com>

3 files changed, 63 insertions(+), 6 deletions(-)

docs/internals/mobile-navigation.md+8
...@@ -37,6 +37,14 @@ an over-the-air JavaScript update alone is insufficient. The Android view manage...@@ -37,6 +37,14 @@ an over-the-air JavaScript update alone is insufficient. The Android view manage
37implements the generated identifier setter as a no-op because this behavior is37implements the generated identifier setter as a no-op because this behavior is
38specific to iOS 26 and later.38specific to iOS 26 and later.
3939
40On iOS 26 the full-screen back swipe is UIKit's `interactiveContentPopGestureRecognizer`,
41and react-native-screens makes it wait for any horizontally scrollable ScrollView to
42fail first. Upstream applies that to every horizontal ScrollView regardless of
43position, so a back swipe on a code block or table that is already at its leading
44edge only bounces. The patch narrows the rule to ScrollViews with content still
45hidden to the left; at the leading edge they yield to the pop gesture like plain
46text does.
47
40After changing a dependency patch, refresh CocoaPods before rebuilding an48After changing a dependency patch, refresh CocoaPods before rebuilding an
41existing iOS project. pnpm installs each patch hash in a different directory;49existing iOS project. pnpm installs each patch hash in a different directory;
42an old Pods project can keep compiling the previous directory even though Metro50an old Pods project can keep compiling the previous directory even though Metro
patches/react-native-screens@4.26.2.patch+49
...@@ -1106,6 +1106,55 @@ index 1c844846a5c66e31cfa530ca462774d2fb6bbea1..a60b96b44e85d3f4b29dc0d190b95ab6...@@ -1106,6 +1106,55 @@ index 1c844846a5c66e31cfa530ca462774d2fb6bbea1..a60b96b44e85d3f4b29dc0d190b95ab6
1106 [self updateViewControllerIfNeeded];1106 [self updateViewControllerIfNeeded];
1107 1107
1108 if (needsNavigationControllerLayout) {1108 if (needsNavigationControllerLayout) {
1109diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm
1110--- a/ios/RNSScreenStack.mm
1111+++ b/ios/RNSScreenStack.mm
1112@@ -1074,6 +1074,15 @@ RNS_IGNORE_SUPER_CALL_END
1113 return scrollView.contentSize.width > scrollView.frame.size.width;
1114 }
1115
1116+// Whether a horizontally scrollable ScrollView is resting at its leading edge, so a
1117+// pan in the back-gesture direction has nothing left to scroll. Checked in the
1118+// ScrollView's own coordinate space: React Native mirrors the view for RTL layout,
1119+// which keeps the leading edge at the minimum content offset in both directions.
1120+- (BOOL)scrollViewIsAtLeadingEdge:(UIScrollView *)scrollView
1121+{
1122+ return scrollView.contentOffset.x <= -scrollView.adjustedContentInset.left + 1.0;
1123+}
1124+
1125 // Custom method for compatibility with iOS < 13.4
1126 // RNSScreenStackView is a UIGestureRecognizerDelegate for three types of gesture recognizers:
1127 // RNSPanGestureRecognizer, RNSScreenEdgeGestureRecognizer, _UIParallaxTransitionPanGestureRecognizer
1128@@ -1167,9 +1176,11 @@ RNS_IGNORE_SUPER_CALL_END
1129 if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer &&
1130 [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) {
1131 // ScrollView should take precedence when scrolling horizontally (it should be required to fail).
1132- // However, if it does not allow for horizontal scrolling, there should be no such restriction,
1133+ // However, if it does not allow for horizontal scrolling, or is already resting at its leading
1134+ // edge so a back swipe has nothing left to scroll, there should be no such restriction,
1135 // and swiping horizontally should dismiss the screen.
1136- return [self scrollViewHasHorizontalPart:static_cast<UIScrollView *>(otherGestureRecognizer.view)];
1137+ UIScrollView *scrollView = static_cast<UIScrollView *>(otherGestureRecognizer.view);
1138+ return [self scrollViewHasHorizontalPart:scrollView] && ![self scrollViewIsAtLeadingEdge:scrollView];
1139 }
1140 }
1141
1142@@ -1189,6 +1200,15 @@ RNS_IGNORE_SUPER_CALL_END
1143 // gestureRecognizer:shouldRequireFailureOfGestureRecognizer
1144 isEdgeSwipeGestureRecognizer =
1145 isEdgeSwipeGestureRecognizer && gestureRecognizer != _controller.interactiveContentPopGestureRecognizer;
1146+
1147+ // The exception is a horizontal ScrollView resting at its leading edge: its pan recognizer would
1148+ // still begin (and merely bounce) on a back swipe, so it must yield to the pop gesture the same
1149+ // way plain content does.
1150+ if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer &&
1151+ [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) {
1152+ UIScrollView *scrollView = static_cast<UIScrollView *>(otherGestureRecognizer.view);
1153+ return [self scrollViewHasHorizontalPart:scrollView] && [self scrollViewIsAtLeadingEdge:scrollView];
1154+ }
1155 }
1156 #endif // check for iOS >= 26
1157
1109diff --git a/ios/RNSScreenStackHeaderSubview.mm b/ios/RNSScreenStackHeaderSubview.mm1158diff --git a/ios/RNSScreenStackHeaderSubview.mm b/ios/RNSScreenStackHeaderSubview.mm
1110index add33c4807e038dcd846f6c72b2fc472ded02809..489afa5fa2da09c1ec6986d1832b58bb595396ae 1006441159index add33c4807e038dcd846f6c72b2fc472ded02809..489afa5fa2da09c1ec6986d1832b58bb595396ae 100644
1111--- a/ios/RNSScreenStackHeaderSubview.mm1160--- a/ios/RNSScreenStackHeaderSubview.mm
pnpm-lock.yaml+6-6
...@@ -102,7 +102,7 @@ patchedDependencies:...@@ -102,7 +102,7 @@ patchedDependencies:
102 react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3102 react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3
103 react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787103 react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787
104 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675104 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675
105 react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006105 react-native-screens@4.26.2: 8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d
106 uniwind@1.11.0: 17d92be2eec71bb6396b402e8d034968e54b28746876d7977cb3139655f42b90106 uniwind@1.11.0: 17d92be2eec71bb6396b402e8d034968e54b28746876d7977cb3139655f42b90
107107
108importers:108importers:
...@@ -254,7 +254,7 @@ importers:...@@ -254,7 +254,7 @@ importers:
254 version: 7.3.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)254 version: 7.3.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
255 '@react-navigation/native-stack':255 '@react-navigation/native-stack':
256 specifier: 7.17.6256 specifier: 7.17.6
257 version: 7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(d307537762dff86bcf277a4ec64a11d8)257 version: 7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(ad1eff2c3e588b799b6541240bb21d97)
258 '@shikijs/core':258 '@shikijs/core':
259 specifier: 4.2.0259 specifier: 4.2.0
260 version: 4.2.0260 version: 4.2.0
...@@ -428,7 +428,7 @@ importers:...@@ -428,7 +428,7 @@ importers:
428 version: 5.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)428 version: 5.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
429 react-native-screens:429 react-native-screens:
430 specifier: ~4.26.0430 specifier: ~4.26.0
431 version: 4.26.2(patch_hash=149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)431 version: 4.26.2(patch_hash=8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
432 react-native-shiki-engine:432 react-native-shiki-engine:
433 specifier: ^0.3.12433 specifier: ^0.3.12
434 version: 0.3.12(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)434 version: 0.3.12(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
...@@ -13876,7 +13876,7 @@ snapshots:...@@ -13876,7 +13876,7 @@ snapshots:
13876 optionalDependencies:13876 optionalDependencies:
13877 '@react-native-masked-view/masked-view': 0.3.2(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13877 '@react-native-masked-view/masked-view': 0.3.2(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
1387813878
13879 '@react-navigation/native-stack@7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(d307537762dff86bcf277a4ec64a11d8)':13879 '@react-navigation/native-stack@7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(ad1eff2c3e588b799b6541240bb21d97)':
13880 dependencies:13880 dependencies:
13881 '@react-navigation/elements': 2.9.26(c10301b6e0c42fc6434d2b643197a81e)13881 '@react-navigation/elements': 2.9.26(c10301b6e0c42fc6434d2b643197a81e)
13882 '@react-navigation/native': 7.3.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13882 '@react-navigation/native': 7.3.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
...@@ -13884,7 +13884,7 @@ snapshots:...@@ -13884,7 +13884,7 @@ snapshots:
13884 react: 19.2.313884 react: 19.2.3
13885 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)13885 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
13886 react-native-safe-area-context: 5.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13886 react-native-safe-area-context: 5.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
13887 react-native-screens: 4.26.2(patch_hash=149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)13887 react-native-screens: 4.26.2(patch_hash=8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
13888 sf-symbols-typescript: 2.2.013888 sf-symbols-typescript: 2.2.0
13889 warn-once: 0.1.113889 warn-once: 0.1.1
13890 transitivePeerDependencies:13890 transitivePeerDependencies:
...@@ -19449,7 +19449,7 @@ snapshots:...@@ -19449,7 +19449,7 @@ snapshots:
19449 react: 19.2.319449 react: 19.2.3
19450 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)19450 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
1945119451
19452 react-native-screens@4.26.2(patch_hash=149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):19452 react-native-screens@4.26.2(patch_hash=8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3):
19453 dependencies:19453 dependencies:
19454 react: 19.2.319454 react: 19.2.3
19455 react-freeze: 1.0.4(react@19.2.3)19455 react-freeze: 1.0.4(react@19.2.3)