diff --git a/docs/internals/mobile-navigation.md b/docs/internals/mobile-navigation.md index 12b0040d4bd2..c2528219ac6b 100644 --- a/docs/internals/mobile-navigation.md +++ b/docs/internals/mobile-navigation.md @@ -37,6 +37,14 @@ an over-the-air JavaScript update alone is insufficient. The Android view manage implements the generated identifier setter as a no-op because this behavior is specific to iOS 26 and later. +On iOS 26 the full-screen back swipe is UIKit's `interactiveContentPopGestureRecognizer`, +and react-native-screens makes it wait for any horizontally scrollable ScrollView to +fail first. Upstream applies that to every horizontal ScrollView regardless of +position, so a back swipe on a code block or table that is already at its leading +edge only bounces. The patch narrows the rule to ScrollViews with content still +hidden to the left; at the leading edge they yield to the pop gesture like plain +text does. + After changing a dependency patch, refresh CocoaPods before rebuilding an existing iOS project. pnpm installs each patch hash in a different directory; an old Pods project can keep compiling the previous directory even though Metro diff --git a/patches/react-native-screens@4.26.2.patch b/patches/react-native-screens@4.26.2.patch index ff5168e8c692..ee017ce1993f 100644 --- a/patches/react-native-screens@4.26.2.patch +++ b/patches/react-native-screens@4.26.2.patch @@ -1106,6 +1106,55 @@ index 1c844846a5c66e31cfa530ca462774d2fb6bbea1..a60b96b44e85d3f4b29dc0d190b95ab6 [self updateViewControllerIfNeeded]; if (needsNavigationControllerLayout) { +diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm +--- a/ios/RNSScreenStack.mm ++++ b/ios/RNSScreenStack.mm +@@ -1074,6 +1074,15 @@ RNS_IGNORE_SUPER_CALL_END + return scrollView.contentSize.width > scrollView.frame.size.width; + } + ++// Whether a horizontally scrollable ScrollView is resting at its leading edge, so a ++// pan in the back-gesture direction has nothing left to scroll. Checked in the ++// ScrollView's own coordinate space: React Native mirrors the view for RTL layout, ++// which keeps the leading edge at the minimum content offset in both directions. ++- (BOOL)scrollViewIsAtLeadingEdge:(UIScrollView *)scrollView ++{ ++ return scrollView.contentOffset.x <= -scrollView.adjustedContentInset.left + 1.0; ++} ++ + // Custom method for compatibility with iOS < 13.4 + // RNSScreenStackView is a UIGestureRecognizerDelegate for three types of gesture recognizers: + // RNSPanGestureRecognizer, RNSScreenEdgeGestureRecognizer, _UIParallaxTransitionPanGestureRecognizer +@@ -1167,9 +1176,11 @@ RNS_IGNORE_SUPER_CALL_END + if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer && + [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) { + // ScrollView should take precedence when scrolling horizontally (it should be required to fail). +- // However, if it does not allow for horizontal scrolling, there should be no such restriction, ++ // However, if it does not allow for horizontal scrolling, or is already resting at its leading ++ // edge so a back swipe has nothing left to scroll, there should be no such restriction, + // and swiping horizontally should dismiss the screen. +- return [self scrollViewHasHorizontalPart:static_cast(otherGestureRecognizer.view)]; ++ UIScrollView *scrollView = static_cast(otherGestureRecognizer.view); ++ return [self scrollViewHasHorizontalPart:scrollView] && ![self scrollViewIsAtLeadingEdge:scrollView]; + } + } + +@@ -1189,6 +1200,15 @@ RNS_IGNORE_SUPER_CALL_END + // gestureRecognizer:shouldRequireFailureOfGestureRecognizer + isEdgeSwipeGestureRecognizer = + isEdgeSwipeGestureRecognizer && gestureRecognizer != _controller.interactiveContentPopGestureRecognizer; ++ ++ // The exception is a horizontal ScrollView resting at its leading edge: its pan recognizer would ++ // still begin (and merely bounce) on a back swipe, so it must yield to the pop gesture the same ++ // way plain content does. ++ if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer && ++ [self isScrollViewPanGestureRecognizer:otherGestureRecognizer]) { ++ UIScrollView *scrollView = static_cast(otherGestureRecognizer.view); ++ return [self scrollViewHasHorizontalPart:scrollView] && [self scrollViewIsAtLeadingEdge:scrollView]; ++ } + } + #endif // check for iOS >= 26 + diff --git a/ios/RNSScreenStackHeaderSubview.mm b/ios/RNSScreenStackHeaderSubview.mm index add33c4807e038dcd846f6c72b2fc472ded02809..489afa5fa2da09c1ec6986d1832b58bb595396ae 100644 --- a/ios/RNSScreenStackHeaderSubview.mm diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76cf816327be..bf96298e53df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,7 +102,7 @@ patchedDependencies: react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 + react-native-screens@4.26.2: 8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d uniwind@1.11.0: 17d92be2eec71bb6396b402e8d034968e54b28746876d7977cb3139655f42b90 importers: @@ -254,7 +254,7 @@ importers: 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) '@react-navigation/native-stack': specifier: 7.17.6 - version: 7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(d307537762dff86bcf277a4ec64a11d8) + version: 7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(ad1eff2c3e588b799b6541240bb21d97) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -428,7 +428,7 @@ importers: 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) react-native-screens: specifier: ~4.26.0 - 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) + 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) react-native-shiki-engine: specifier: ^0.3.12 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: optionalDependencies: '@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) - '@react-navigation/native-stack@7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(d307537762dff86bcf277a4ec64a11d8)': + '@react-navigation/native-stack@7.17.6(patch_hash=e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552)(ad1eff2c3e588b799b6541240bb21d97)': dependencies: '@react-navigation/elements': 2.9.26(c10301b6e0c42fc6434d2b643197a81e) '@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: react: 19.2.3 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-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) - 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) + 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) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: @@ -19449,7 +19449,7 @@ snapshots: react: 19.2.3 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-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): + 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): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3)