| author | |
| committer | |
| log | 80b53730d1e821563e5c348fdf4f5fc7616a49f7 |
| tree | 8f3590ed8a4f09e2ec9200742a7cd4691d5f1c18 |
| parent | 409bc4fa6f3bb9869052bdba4244b30606457f20 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 37 | implements the generated identifier setter as a no-op because this behavior is |
| 38 | 38 | specific to iOS 26 and later. |
| 39 | 39 | |
| 40 | On iOS 26 the full-screen back swipe is UIKit's `interactiveContentPopGestureRecognizer`, | |
| 41 | and react-native-screens makes it wait for any horizontally scrollable ScrollView to | |
| 42 | fail first. Upstream applies that to every horizontal ScrollView regardless of | |
| 43 | position, so a back swipe on a code block or table that is already at its leading | |
| 44 | edge only bounces. The patch narrows the rule to ScrollViews with content still | |
| 45 | hidden to the left; at the leading edge they yield to the pop gesture like plain | |
| 46 | text does. | |
| 47 | ||
| 40 | 48 | After changing a dependency patch, refresh CocoaPods before rebuilding an |
| 41 | 49 | existing iOS project. pnpm installs each patch hash in a different directory; |
| 42 | 50 | an 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 | 1106 | [self updateViewControllerIfNeeded]; |
| 1107 | 1107 | |
| 1108 | 1108 | if (needsNavigationControllerLayout) { |
| 1109 | diff --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 | ||
| 1109 | 1158 | diff --git a/ios/RNSScreenStackHeaderSubview.mm b/ios/RNSScreenStackHeaderSubview.mm |
| 1110 | 1159 | index add33c4807e038dcd846f6c72b2fc472ded02809..489afa5fa2da09c1ec6986d1832b58bb595396ae 100644 |
| 1111 | 1160 | --- a/ios/RNSScreenStackHeaderSubview.mm |
pnpm-lock.yaml+6-6| ... | ... | @@ -102,7 +102,7 @@ patchedDependencies: |
| 102 | 102 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 |
| 103 | 103 | react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 |
| 104 | 104 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 |
| 105 | react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 | |
| 105 | react-native-screens@4.26.2: 8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d | |
| 106 | 106 | uniwind@1.11.0: 17d92be2eec71bb6396b402e8d034968e54b28746876d7977cb3139655f42b90 |
| 107 | 107 | |
| 108 | 108 | importers: |
| ... | ... | @@ -254,7 +254,7 @@ importers: |
| 254 | 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 | 255 | '@react-navigation/native-stack': |
| 256 | 256 | specifier: 7.17.6 |
| 257 | version: 7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(d307537762dff86bcf277a4ec64a11d8) | |
| 257 | version: 7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(ad1eff2c3e588b799b6541240bb21d97) | |
| 258 | 258 | '@shikijs/core': |
| 259 | 259 | specifier: 4.2.0 |
| 260 | 260 | version: 4.2.0 |
| ... | ... | @@ -428,7 +428,7 @@ importers: |
| 428 | 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 | 429 | react-native-screens: |
| 430 | 430 | 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 | 432 | react-native-shiki-engine: |
| 433 | 433 | specifier: ^0.3.12 |
| 434 | 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 | 13876 | optionalDependencies: |
| 13877 | 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) |
| 13878 | 13878 | |
| 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 | 13880 | dependencies: |
| 13881 | 13881 | '@react-navigation/elements': 2.9.26(c10301b6e0c42fc6434d2b643197a81e) |
| 13882 | 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 | 13884 | react: 19.2.3 |
| 13885 | 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 | 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 | 13888 | sf-symbols-typescript: 2.2.0 |
| 13889 | 13889 | warn-once: 0.1.1 |
| 13890 | 13890 | transitivePeerDependencies: |
| ... | ... | @@ -19449,7 +19449,7 @@ snapshots: |
| 19449 | 19449 | react: 19.2.3 |
| 19450 | 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) |
| 19451 | 19451 | |
| 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 | 19453 | dependencies: |
| 19454 | 19454 | react: 19.2.3 |
| 19455 | 19455 | react-freeze: 1.0.4(react@19.2.3) |