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..2e3ad5f9 --- /dev/null +++ b/ios/PagerGestureDelegate.swift @@ -0,0 +1,86 @@ +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 + private var gestureObserver: NSKeyValueObservation? + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + // 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, *) { + // 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 + + // 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 { + // Ensure UIKit updates happen on main queue + DispatchQueue.main.async { + collectionView.panGestureRecognizer.isEnabled = self.scrollEnabled + } + self.gestureObserver?.invalidate() + self.gestureObserver = nil + } + } + } else { + collectionView.panGestureRecognizer.isEnabled = scrollEnabled + } + + return true + } + + deinit { + gestureObserver?.invalidate() + } +} 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 + } } }