From 0a818692543a57f0aea70978233595acb5f6acf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:48:43 +0000 Subject: [PATCH 1/4] Initial plan From 716229b1cdcd754e50e252289608b956baf51029 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:55:55 +0000 Subject: [PATCH 2/4] Add iOS 26 back gesture support for pager view Co-authored-by: troZee <12766071+troZee@users.noreply.github.com> --- .gitignore | 1 + ios/PagerGestureDelegate.swift | 55 ++++++++++++++++++++++++++++++++++ ios/PagerView.swift | 13 ++++++++ 3 files changed, 69 insertions(+) create mode 100644 ios/PagerGestureDelegate.swift diff --git a/.gitignore b/.gitignore index 138a447b..b8f3f893 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ node_modules/ npm-debug.log yarn-debug.log yarn-error.log +package-lock.json # Expo .expo/* diff --git a/ios/PagerGestureDelegate.swift b/ios/PagerGestureDelegate.swift new file mode 100644 index 00000000..9645046c --- /dev/null +++ b/ios/PagerGestureDelegate.swift @@ -0,0 +1,55 @@ +import UIKit + +/** + Gesture recognizer delegate to handle iOS 26+ interactiveContentPopGestureRecognizer. + Allows navigation back gesture on first page while preserving pager functionality. + */ +class PagerGestureDelegate: NSObject, UIGestureRecognizerDelegate { + weak var collectionView: UICollectionView? + var currentPage: Int = 0 + var scrollEnabled: Bool = true + var layoutDirection: PagerLayoutDirection = .ltr + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + // iOS 26+ full-screen back gesture (interactiveContentPopGestureRecognizer) + if #available(iOS 26, *) { + // Get the navigation controller's interactive content pop gesture recognizer + guard let collectionView = collectionView, + let viewController = collectionView.reactViewController(), + let navigationController = viewController.navigationController else { + return false + } + + // Check if this is the pager's pan gesture and the other is the navigation back gesture + if gestureRecognizer == collectionView.panGestureRecognizer, + otherGestureRecognizer == navigationController.interactiveContentPopGestureRecognizer { + + // Get velocity to determine swipe direction + guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + + let velocity = panGestureRecognizer.velocity(in: collectionView) + let isLTR = layoutDirection == .ltr + let isBackGesture = (isLTR && velocity.x > 0) || (!isLTR && velocity.x < 0) + + // If on first page and performing back gesture, disable pager scroll to allow navigation + if currentPage == 0 && isBackGesture { + collectionView.panGestureRecognizer.isEnabled = false + + // Re-enable after gesture ends to restore normal behavior + DispatchQueue.main.async { [weak self, weak collectionView] in + guard let self = self, let collectionView = collectionView else { return } + collectionView.panGestureRecognizer.isEnabled = self.scrollEnabled + } + } else { + collectionView.panGestureRecognizer.isEnabled = scrollEnabled + } + + return true + } + } + + return false + } +} diff --git a/ios/PagerView.swift b/ios/PagerView.swift index 4b866290..1959afc9 100644 --- a/ios/PagerView.swift +++ b/ios/PagerView.swift @@ -4,6 +4,7 @@ import SwiftUI struct PagerView: View { @ObservedObject var props: PagerViewProps @State private var scrollDelegate = PagerScrollDelegate() + @State private var gestureDelegate = PagerGestureDelegate() weak var delegate: PagerViewProviderDelegate? @Weak var collectionView: UICollectionView? @@ -37,6 +38,13 @@ struct PagerView: View { scrollDelegate.orientation = props.orientation collectionView.delegate = scrollDelegate } + + // Set up gesture delegate for iOS 26+ back gesture handling + gestureDelegate.collectionView = collectionView + gestureDelegate.currentPage = props.currentPage + gestureDelegate.scrollEnabled = props.scrollEnabled + gestureDelegate.layoutDirection = props.layoutDirection + collectionView.panGestureRecognizer.delegate = gestureDelegate } .onChange(of: props.children) { newValue in if props.currentPage >= newValue.count && !newValue.isEmpty { @@ -45,9 +53,11 @@ struct PagerView: View { } .onChange(of: props.currentPage) { newValue in delegate?.onPageSelected(position: newValue) + gestureDelegate.currentPage = newValue } .onChange(of: props.scrollEnabled) { newValue in collectionView?.isScrollEnabled = newValue + gestureDelegate.scrollEnabled = newValue } .onChange(of: props.overdrag) { newValue in collectionView?.bounces = newValue @@ -55,5 +65,8 @@ struct PagerView: View { .onChange(of: props.keyboardDismissMode) { newValue in collectionView?.keyboardDismissMode = newValue } + .onChange(of: props.layoutDirection) { newValue in + gestureDelegate.layoutDirection = newValue + } } } From 57dab2d3c6c21ae87e597efd76e6c79d22be5e50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:57:39 +0000 Subject: [PATCH 3/4] Improve gesture handling with KVO and fallback logic Co-authored-by: troZee <12766071+troZee@users.noreply.github.com> --- ios/PagerGestureDelegate.swift | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/ios/PagerGestureDelegate.swift b/ios/PagerGestureDelegate.swift index 9645046c..8e82d993 100644 --- a/ios/PagerGestureDelegate.swift +++ b/ios/PagerGestureDelegate.swift @@ -9,6 +9,7 @@ class PagerGestureDelegate: NSObject, UIGestureRecognizerDelegate { var currentPage: Int = 0 var scrollEnabled: Bool = true var layoutDirection: PagerLayoutDirection = .ltr + private var gestureObserver: NSKeyValueObservation? func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // iOS 26+ full-screen back gesture (interactiveContentPopGestureRecognizer) @@ -20,9 +21,19 @@ class PagerGestureDelegate: NSObject, UIGestureRecognizerDelegate { return false } + // iOS 26 introduces interactiveContentPopGestureRecognizer for full-screen back gestures + // We need to check if this property exists and use it, otherwise fall back to standard behavior + let interactiveGesture: UIGestureRecognizer? + if let contentPopGesture = navigationController.value(forKey: "interactiveContentPopGestureRecognizer") as? UIGestureRecognizer { + interactiveGesture = contentPopGesture + } else { + // Fallback to standard interactivePopGestureRecognizer + interactiveGesture = navigationController.interactivePopGestureRecognizer + } + // Check if this is the pager's pan gesture and the other is the navigation back gesture if gestureRecognizer == collectionView.panGestureRecognizer, - otherGestureRecognizer == navigationController.interactiveContentPopGestureRecognizer { + otherGestureRecognizer == interactiveGesture { // Get velocity to determine swipe direction guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { @@ -37,10 +48,19 @@ class PagerGestureDelegate: NSObject, UIGestureRecognizerDelegate { if currentPage == 0 && isBackGesture { collectionView.panGestureRecognizer.isEnabled = false - // Re-enable after gesture ends to restore normal behavior - DispatchQueue.main.async { [weak self, weak collectionView] in - guard let self = self, let collectionView = collectionView else { return } - collectionView.panGestureRecognizer.isEnabled = self.scrollEnabled + // Observe gesture state to re-enable when gesture ends + gestureObserver?.invalidate() + gestureObserver = otherGestureRecognizer.observe(\.state, options: [.new]) { [weak self, weak collectionView] _, change in + guard let self = self, + let collectionView = collectionView, + let newState = change.newValue else { return } + + // Re-enable pager scroll when navigation gesture ends + if newState == .ended || newState == .cancelled || newState == .failed { + collectionView.panGestureRecognizer.isEnabled = self.scrollEnabled + self.gestureObserver?.invalidate() + self.gestureObserver = nil + } } } else { collectionView.panGestureRecognizer.isEnabled = scrollEnabled @@ -52,4 +72,8 @@ class PagerGestureDelegate: NSObject, UIGestureRecognizerDelegate { return false } + + deinit { + gestureObserver?.invalidate() + } } From cb7013f0a90208106de3a2ccb31c184a8de61104 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:59:21 +0000 Subject: [PATCH 4/4] Fix thread safety and improve API access pattern Co-authored-by: troZee <12766071+troZee@users.noreply.github.com> --- ios/PagerGestureDelegate.swift | 107 ++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/ios/PagerGestureDelegate.swift b/ios/PagerGestureDelegate.swift index 8e82d993..2e3ad5f9 100644 --- a/ios/PagerGestureDelegate.swift +++ b/ios/PagerGestureDelegate.swift @@ -12,65 +12,72 @@ class PagerGestureDelegate: NSObject, UIGestureRecognizerDelegate { private var gestureObserver: NSKeyValueObservation? func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - // iOS 26+ full-screen back gesture (interactiveContentPopGestureRecognizer) + // Get the navigation controller + guard let collectionView = collectionView, + let viewController = collectionView.reactViewController(), + let navigationController = viewController.navigationController else { + return false + } + + // Check if this is the pager's pan gesture + guard gestureRecognizer == collectionView.panGestureRecognizer else { + return false + } + + // Determine which navigation gesture recognizer to check + var navGestureRecognizer: UIGestureRecognizer? = navigationController.interactivePopGestureRecognizer + + // iOS 26+ introduces interactiveContentPopGestureRecognizer for full-screen back gestures if #available(iOS 26, *) { - // Get the navigation controller's interactive content pop gesture recognizer - guard let collectionView = collectionView, - let viewController = collectionView.reactViewController(), - let navigationController = viewController.navigationController else { - return false - } - - // iOS 26 introduces interactiveContentPopGestureRecognizer for full-screen back gestures - // We need to check if this property exists and use it, otherwise fall back to standard behavior - let interactiveGesture: UIGestureRecognizer? - if let contentPopGesture = navigationController.value(forKey: "interactiveContentPopGestureRecognizer") as? UIGestureRecognizer { - interactiveGesture = contentPopGesture - } else { - // Fallback to standard interactivePopGestureRecognizer - interactiveGesture = navigationController.interactivePopGestureRecognizer + // Try to access the new property safely using selector + let selector = NSSelectorFromString("interactiveContentPopGestureRecognizer") + if navigationController.responds(to: selector), + let contentPopGesture = navigationController.perform(selector)?.takeUnretainedValue() as? UIGestureRecognizer { + navGestureRecognizer = contentPopGesture } + } + + // Check if the other gesture is the navigation back gesture + guard let navGesture = navGestureRecognizer, + otherGestureRecognizer == navGesture else { + return false + } + + // Get velocity to determine swipe direction + guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + + let velocity = panGestureRecognizer.velocity(in: collectionView) + let isLTR = layoutDirection == .ltr + let isBackGesture = (isLTR && velocity.x > 0) || (!isLTR && velocity.x < 0) + + // If on first page and performing back gesture, disable pager scroll to allow navigation + if currentPage == 0 && isBackGesture { + collectionView.panGestureRecognizer.isEnabled = false - // Check if this is the pager's pan gesture and the other is the navigation back gesture - if gestureRecognizer == collectionView.panGestureRecognizer, - otherGestureRecognizer == interactiveGesture { + // Observe gesture state to re-enable when gesture ends + gestureObserver?.invalidate() + gestureObserver = otherGestureRecognizer.observe(\.state, options: [.new]) { [weak self, weak collectionView] _, change in + guard let self = self, + let collectionView = collectionView, + let newState = change.newValue else { return } - // Get velocity to determine swipe direction - guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { - return false - } - - let velocity = panGestureRecognizer.velocity(in: collectionView) - let isLTR = layoutDirection == .ltr - let isBackGesture = (isLTR && velocity.x > 0) || (!isLTR && velocity.x < 0) - - // If on first page and performing back gesture, disable pager scroll to allow navigation - if currentPage == 0 && isBackGesture { - collectionView.panGestureRecognizer.isEnabled = false - - // Observe gesture state to re-enable when gesture ends - gestureObserver?.invalidate() - gestureObserver = otherGestureRecognizer.observe(\.state, options: [.new]) { [weak self, weak collectionView] _, change in - guard let self = self, - let collectionView = collectionView, - let newState = change.newValue else { return } - - // Re-enable pager scroll when navigation gesture ends - if newState == .ended || newState == .cancelled || newState == .failed { - collectionView.panGestureRecognizer.isEnabled = self.scrollEnabled - self.gestureObserver?.invalidate() - self.gestureObserver = nil - } + // Re-enable pager scroll when navigation gesture ends + if newState == .ended || newState == .cancelled || newState == .failed { + // Ensure UIKit updates happen on main queue + DispatchQueue.main.async { + collectionView.panGestureRecognizer.isEnabled = self.scrollEnabled } - } else { - collectionView.panGestureRecognizer.isEnabled = scrollEnabled + self.gestureObserver?.invalidate() + self.gestureObserver = nil } - - return true } + } else { + collectionView.panGestureRecognizer.isEnabled = scrollEnabled } - return false + return true } deinit {