Skip to content

[iOS] On iPad, it is conflicting header and back button. Also active tab indication not worked. #1067

Open

Description

@prakashpro3

Environment

In iPhone and android device works fine. But in iPad issue occur.

Description

On iPad, it is conflicting header and back button. Also active tab indication not worked.
After downgrading to 7.0.2 works fine.

Reproducible Demo

Image

issue in latest v8.0.0.

Activity

  1. prakashpro3 commented on Apr 3, 2026

    @prakashpro3
    Author

    After downgrading to 7.0.2 works fine.

  2. ndr commented on Apr 15, 2026

    @ndr

    The same on iphone7 with ios 15.8

  3. manuhook commented on Apr 24, 2026

    @manuhook

    Here's a patch for iOS 15 support

    react-native-pager-view : 8.0.1
    react-native : 0.85.2
    react-native-tab-view : 4.3.0

    index 8c01e80b785652d9777b9a8e52cc892cb7140b2c..10bd84f996ff5e4d9d9d3e5009fd8929cb2210cb 100644
    --- a/lib/module/Pager.ios.js
    +++ b/lib/module/Pager.ios.js
    @@ -1,4 +1,11 @@
     "use strict";
     
    -export { PagerViewAdapter as Pager } from "./PagerViewAdapter.js";
    +import { Platform } from 'react-native';
    +import { PanResponderAdapter } from "./PanResponderAdapter.js";
    +import { PagerViewAdapter } from "./PagerViewAdapter.js";
    +
    +// react-native-pager-view has a bug on iOS < 16 + Fabric:
    +// onPageScroll events don't propagate Animated.Values properly.
    +const iosVersion = parseFloat(String(Platform.Version));
    +export const Pager = iosVersion < 16 ? PanResponderAdapter : PagerViewAdapter;
     //# sourceMappingURL=Pager.ios.js.map
    diff --git a/src/Pager.ios.tsx b/src/Pager.ios.tsx
    index 3358dadcaf965abafeb3688d863ccfd2c29b2dce..b37a41cd20cfe5c8007a8a5ae426e95614b66c7e 100644
    --- a/src/Pager.ios.tsx
    +++ b/src/Pager.ios.tsx
    @@ -1 +1,8 @@
    -export { PagerViewAdapter as Pager } from './PagerViewAdapter';
    +import { Platform } from 'react-native';
    +import { PanResponderAdapter } from './PanResponderAdapter';
    +import { PagerViewAdapter } from './PagerViewAdapter';
    +
    +// react-native-pager-view has a bug on iOS < 16 + Fabric:
    +// onPageScroll events don't propagate Animated.Values properly.
    +const iosVersion = parseFloat(String(Platform.Version));
    +export const Pager = iosVersion < 16 ? PanResponderAdapter : PagerViewAdapter;
  4. SenglayPann commented on May 21, 2026

    @SenglayPann

    The same on iphone7 with ios 15.8

    Downgrading to v 7.0.2 will resolve the issue

  5. rissajeanne commented on Aug 8, 2026

    @rissajeanne

    Here's a patch for iOS 15 support

    @manuhook Mentioning to hopefully help others... this patch is for react-native-tab-view and not react-native-pager-view.

  6. gaodeng commented on Oct 17, 2026

    @gaodeng

    Confirming this on an iPad running iOS 15.x with react-native-pager-view 9.0.9. We observed three symptoms together and confirmed that the combined native workaround described below resolves all three in our app.

    This overlaps #1080. The embedded scroll-delegate change is based on @d-mato's #1179; credit for that fix belongs to that PR. The header and touch-cancellation workarounds below address the additional symptoms we observed. I have not tested each patch component independently, so this is not a claim that all three have one root cause or that #1179 alone fixes all three.

    Environment

    • react-native-pager-view: 9.0.9
    • react-native-tab-view: 4.3.4
    • react-native-screens: 4.30.0
    • @react-navigation/native-stack: 7.21.0
    • React Native: 0.87.1, New Architecture / Fabric
    • Affected device: iPad, iOS 15.x (exact model and minor OS version were not recorded for this report)

    Reproduction outline

    These steps describe our app's setup; I have not extracted a standalone reproduction repository yet.

    1. Use a native stack with a root screen, a search screen, and a plain screen without a pager. Configure the search screen with headerShown: false and render a custom TextInput at the top.
    2. In the search screen, render react-native-tab-view with three routes. Pages contain tappable items whose onPress navigates to a detail screen.
    3. Push the search screen on an iPad running iOS 15.
    4. Switch tabs by tapping their labels and by swiping horizontally, starting the swipe on a tappable item.
    5. Navigate from the search screen to the plain screen, then go back.

    Actual behavior

    • A default native navigation bar appears over the custom search input despite headerShown: false, including the route title and native back button. Opening the plain non-pager screen and returning makes this unwanted bar disappear. The plain screen itself does not show the unwanted header.
    • Page content switches, but the tab indicator does not track the selected page.
    • A horizontal swipe can both change pages and trigger the onPress of the item where the gesture started, unexpectedly navigating to its detail screen on release.
    • Delaying input focus until the native-stack transition finishes did not resolve the header issue.

    Native workaround

    A PanResponderAdapter fallback on iOS 15 avoided the symptoms, but degraded paging performance in our app. We replaced it with a native patch, retaining the normal PagerViewAdapter.

    The combined patch:

    • Proxies the embedded orthogonal paging scroll view's UIScrollViewDelegate, forwarding calls to its original delegate, following fix(ios): emit onPageScroll and onPageScrollStateChanged on iOS 15 #1179.
    • Retries attachment for initial layout and later page creation, retains the proxy without retaining its event sink, and avoids installing a proxy in front of itself.
    • Suppresses page-progress/state events from the outer collection view on iOS 15, so outer layout events do not overwrite the actual horizontal paging position.
    • Cancels active ancestor RCTSurfaceTouchHandler presses when the real paging scroller starts dragging on iOS 15. Normal taps and programmatic page changes do not invoke this cancellation path.
    • Prevents the embedded UIHostingController from accessing its ancestor navigation controller on iOS 15, leaving header visibility to native-stack.

    The navigation-controller override and touch-handler reset should be treated as compatibility workarounds for review, not as a proposal for a new public API. The iOS-specific paths are gated to iOS 15; no Android change is included.

    Validation

    • All three original symptoms are confirmed resolved on the affected iOS 15 device with the combined native patch, without the JS pager fallback.
    • Swift sources compile against an iOS 15.1 deployment target; the Objective-C++ bridge passes a syntax check against the generated Swift header.
    • Additional native helper checks passed on an iOS 27 simulator: fractional/final scroll positions, drag/settling/idle events, original-delegate forwarding, repeated installation, traversal stopping at page cells, ancestor touch-handler cancellation without disabling the paging pan, and proxy/event-sink teardown. Those helper checks do not replace the iOS 15 device test or constitute an end-to-end RN gesture test.
    Combined native patch against react-native-pager-view 9.0.9

    Paths are relative to the library root. This includes the #1179-based event fix plus the additional app-validated compatibility changes described above.

    diff --git a/ios/PagerScrollDelegate.swift b/ios/PagerScrollDelegate.swift
    --- a/ios/PagerScrollDelegate.swift
    +++ b/ios/PagerScrollDelegate.swift
    @@ -4,9 +4,10 @@
      Scroll delegate used to control underlying TabView's collection view.
      */
     class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDelegate {
    -  weak var originalDelegate: UICollectionViewDelegate?
    +  weak var originalDelegate: UIScrollViewDelegate?
       weak var delegate: PagerViewProviderDelegate?
       var orientation: UICollectionView.ScrollDirection = .horizontal
    +  var emitsPagingEvents = true
       
       private let handledSelectors: Set<Selector> = [
         #selector(scrollViewDidScroll(_:)),
    @@ -20,6 +21,8 @@
       ]
       
       func scrollViewDidScroll(_ scrollView: UIScrollView) {
    +    defer { originalDelegate?.scrollViewDidScroll?(scrollView) }
    +    guard emitsPagingEvents else { return }
         let isHorizontal = orientation == .horizontal
         let pageSize = isHorizontal ? scrollView.frame.width : scrollView.frame.height
         let contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y
    @@ -31,12 +34,12 @@
         
         let eventData = OnPageScrollEventData(position: position, offset: offset)
         delegate?.onPageScroll(data: eventData)
    -    originalDelegate?.scrollViewDidScroll?(scrollView)
       }
     
       /// Emits a final onPageScroll with offset 0 before transitioning to idle,
       /// clearing any residual floating-point offset from the scroll animation.
       private func emitIdleWithCleanOffset(_ scrollView: UIScrollView) {
    +    guard emitsPagingEvents else { return }
         let isHorizontal = orientation == .horizontal
         let pageSize = isHorizontal ? scrollView.frame.width : scrollView.frame.height
         let contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y
    @@ -51,12 +54,16 @@
       }
       
       func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    -    delegate?.onPageScrollStateChanged(state: .dragging)
    +    if emitsPagingEvents {
    +      delegate?.onPageScrollStateChanged(state: .dragging)
    +    }
         originalDelegate?.scrollViewWillBeginDragging?(scrollView)
       }
       
       func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
    -    delegate?.onPageScrollStateChanged(state: .settling)
    +    if emitsPagingEvents {
    +      delegate?.onPageScrollStateChanged(state: .settling)
    +    }
         originalDelegate?.scrollViewWillBeginDecelerating?(scrollView)
       }
       
    @@ -78,11 +85,13 @@
       }
       
       func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    -    originalDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
    +    (originalDelegate as? UICollectionViewDelegate)?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
       }
       
       func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    -    originalDelegate?.collectionView?(collectionView, willDisplay: cell, forItemAt: indexPath)
    +    // iOS 15 only, see PagerViewIOS15Compat.swift (delete together with the iOS 15 support).
    +    PagerViewIOS15Compat.attachScrollDelegate(to: collectionView, delegate: delegate)
    +    (originalDelegate as? UICollectionViewDelegate)?.collectionView?(collectionView, willDisplay: cell, forItemAt: indexPath)
       }
       
       override func responds(to aSelector: Selector!) -> Bool {
    diff --git a/ios/PagerViewIOS15Compat.swift b/ios/PagerViewIOS15Compat.swift
    --- a/ios/PagerViewIOS15Compat.swift
    +++ b/ios/PagerViewIOS15Compat.swift
    @@ -1,4 +1,21 @@
     import UIKit
    +
    +@objc public final class PagerTouchHandler: NSObject {
    +  @objc public static func cancelTouches(in view: UIView, handlerClass: AnyClass) {
    +    var ancestor: UIView? = view
    +    while let current = ancestor {
    +      for recognizer in current.gestureRecognizers ?? [] {
    +        if recognizer.isKind(of: handlerClass) && recognizer.isEnabled {
    +          // Resetting RN's surface handler dispatches touchCancel to Pressability.
    +          // Keep the native paging pan recognizer running and preserve disabled handlers.
    +          recognizer.isEnabled = false
    +          recognizer.isEnabled = true
    +        }
    +      }
    +      ancestor = current.superview
    +    }
    +  }
    +}
     
     /// Workarounds for the SwiftUI paged `TabView` on iOS 15.
     ///
    @@ -7,14 +24,18 @@
     /// collection view (private class `_UICollectionViewOrthogonalScrollerEmbeddedScrollView`), while
     /// the collection view itself only scrolls vertically. Because `PagerView` can only toggle the
     /// collection view, on iOS 15 `scrollEnabled={false}` / `overdrag={false}` never reached the view
    -/// that actually handles the swipe.
    +/// that actually handles the swipe. For the same reason `PagerScrollDelegate`, installed on the
    +/// collection view, never saw the paging scroll, so `onPageScroll` / `onPageScrollStateChanged`
    +/// were never emitted.
     ///
     /// On iOS 16 and later the collection view itself is the pager, so none of this is needed — the
     /// `#available` checks below make every function in here a no-op on those versions.
     ///
    -/// - Note: Delete this file together with the two `PagerViewIOS15Compat` calls in `PagerView.swift`
    -///   once the minimum supported iOS version is 16.
    +/// - Note: Remove the compatibility calls in `PagerView.swift`, `PagerScrollDelegate.swift`,
    +///   and `RNCPagerViewComponentView.mm` when dropping iOS 15 support.
     enum PagerViewIOS15Compat {
    +  private static var scrollDelegateKey: UInt8 = 0
    +
       /// Mirrors `scrollEnabled` onto the scroll views SwiftUI embedded in the paging collection view.
       static func applyScrollEnabled(_ scrollEnabled: Bool, to collectionView: UICollectionView?) {
         if #available(iOS 16, *) {
    @@ -33,11 +54,39 @@
         pagingScrollViews(in: collectionView).forEach { $0.bounces = overdrag }
       }
     
    +  /// Puts a `PagerScrollDelegate` in front of the delegate of the scroll views SwiftUI embedded in
    +  /// the paging collection view. They only exist once the collection view has laid out its pages.
    +  static func attachScrollDelegate(to collectionView: UICollectionView?, delegate: PagerViewProviderDelegate?) {
    +    if #available(iOS 16, *) {
    +      return
    +    }
    +
    +    for scrollView in pagingScrollViews(in: collectionView) {
    +      scrollView.keyboardDismissMode = collectionView?.keyboardDismissMode ?? .none
    +      installScrollDelegate(on: scrollView, delegate: delegate)
    +    }
    +  }
    +
    +  static func installScrollDelegate(on scrollView: UIScrollView, delegate: PagerViewProviderDelegate?) {
    +    if let scrollDelegate = scrollView.delegate as? PagerScrollDelegate {
    +      scrollDelegate.delegate = delegate
    +      return
    +    }
    +
    +    let scrollDelegate = PagerScrollDelegate()
    +    scrollDelegate.originalDelegate = scrollView.delegate
    +    scrollDelegate.delegate = delegate
    +    // UIScrollView holds its delegate weakly. Retain only the proxy; its original
    +    // delegate and event sink remain weak to avoid cycles during screen teardown.
    +    objc_setAssociatedObject(scrollView, &scrollDelegateKey, scrollDelegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    +    scrollView.delegate = scrollDelegate
    +  }
    +
       /// Scroll views that SwiftUI embedded in the paging collection view.
       ///
       /// Scroll views that belong to the pages' React Native content live inside the paging cells, so
       /// the traversal stops at cell boundaries and never touches the user's own scroll views.
    -  private static func pagingScrollViews(in collectionView: UICollectionView?) -> [UIScrollView] {
    +  static func pagingScrollViews(in collectionView: UICollectionView?) -> [UIScrollView] {
         guard let collectionView else { return [] }
     
         return collectPagingScrollViews(in: collectionView)
    diff --git a/ios/PagerView.swift b/ios/PagerView.swift
    --- a/ios/PagerView.swift
    +++ b/ios/PagerView.swift
    @@ -70,13 +70,21 @@
               }
             }
     
    -        if scrollDelegate.originalDelegate == nil {
    +        scrollDelegate.delegate = delegate
    +        // On iOS 15 only the embedded orthogonal scroller reports page motion.
    +        // Outer collection-view layout events must not reset the indicator to 0.
    +        if #available(iOS 16, *) {
    +          scrollDelegate.emitsPagingEvents = true
    +        } else {
    +          scrollDelegate.emitsPagingEvents = false
    +        }
    +        if collectionView.delegate !== scrollDelegate {
               scrollDelegate.originalDelegate = collectionView.delegate
    -          scrollDelegate.delegate = delegate
               // VTabView-style rotation preserves TabView's horizontal collection view.
               scrollDelegate.orientation = .horizontal
               collectionView.delegate = scrollDelegate
             }
    +        PagerViewIOS15Compat.attachScrollDelegate(to: collectionView, delegate: delegate)
           }
         }
         // GeometryReader is the root since the vertical layout; without this it
    @@ -88,6 +96,9 @@
           DispatchQueue.main.async {
             applyScrollEnabled(props.scrollEnabled)
             applyOverdrag(props.overdrag)
    +        // Initial cells can predate delegate installation. Retry after layout;
    +        // later page creation is covered by collectionView(_:willDisplay:forItemAt:).
    +        PagerViewIOS15Compat.attachScrollDelegate(to: collectionView, delegate: delegate)
           }
     
           // `onChange` does not fire for the initial `currentPage` value. Emit the
    @@ -114,6 +125,7 @@
         }
         .onChange(of: props.keyboardDismissMode) { newValue in
           collectionView?.keyboardDismissMode = newValue
    +      PagerViewIOS15Compat.attachScrollDelegate(to: collectionView, delegate: delegate)
         }
       }
     }
    diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift
    --- a/ios/PagerViewProvider.swift
    +++ b/ios/PagerViewProvider.swift
    @@ -1,5 +1,15 @@
     import SwiftUI
     import UIKit
    +
    +private final class PagerHostingController: UIHostingController<PagerView> {
    +  override var navigationController: UINavigationController? {
    +    if #available(iOS 16, *) {
    +      return super.navigationController
    +    }
    +    // The embedded SwiftUI host must not override native-stack's header state.
    +    return nil
    +  }
    +}
     
     @objc public enum PageScrollState: Int {
       case idle
    @@ -236,10 +246,10 @@
         // has them at full size. `PageChildViewController` reads *this* view's
         // insets (the RN screen's, including a native search bar) and re-injects
         // them; the two are independent.
    -    let hostingController = UIHostingController(
    -      rootView: PagerView(props: props, delegate: delegate),
    -      ignoreSafeArea: true
    +    let hostingController = PagerHostingController(
    +      rootView: PagerView(props: props, delegate: delegate)
         )
    +    hostingController.disableSafeArea()
         self.hostingController = hostingController
     
         parentViewController.addChild(hostingController)
    diff --git a/ios/RNCPagerViewComponentView.mm b/ios/RNCPagerViewComponentView.mm
    --- a/ios/RNCPagerViewComponentView.mm
    +++ b/ios/RNCPagerViewComponentView.mm
    @@ -7,6 +7,7 @@
     
     #import "RCTFabricComponentsPlugins.h"
     #import "React/RCTConversions.h"
    +#import <React/RCTSurfaceTouchHandler.h>
     
     #import "RCTOnPageScrollEvent.h"
     
    @@ -102,17 +103,30 @@
     #pragma mark - PagerViewProviderDelegate
     
     - (void)onPageScrollWithData:(OnPageScrollEventData *)data {
    -  const auto eventEmitter = [self pagerEventEmitter];
       [self sendScrollEventsForPosition:data.position offset:data.offset];
     }
     
     - (void)onPageSelectedWithPosition:(NSInteger)position {
       const auto eventEmitter = [self pagerEventEmitter];
    +  if (!eventEmitter) {
    +    return;
    +  }
       eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position =  static_cast<double>(position)});
     }
     
     - (void)onPageScrollStateChangedWithState:(enum PageScrollState)state {
       const auto eventEmitter = [self pagerEventEmitter];
    +  if (!eventEmitter) {
    +    return;
    +  }
    +
    +  if (@available(iOS 16, *)) {
    +    // UIKit handles touch cancellation on the newer paging implementation.
    +  } else if (state == PageScrollStateDragging) {
    +    // SwiftUI's iOS 15 orthogonal scroller does not cancel RN's active press.
    +    // Reset the ancestor surface touch handlers before a lift can become onPress.
    +    [PagerTouchHandler cancelTouchesIn:self handlerClass:[RCTSurfaceTouchHandler class]];
    +  }
     
       RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState scrollState;
     
    @@ -171,6 +185,9 @@
     
     - (void)sendScrollEventsForPosition:(NSInteger)position offset:(CGFloat)offset {
       const auto eventEmitter = [self pagerEventEmitter];
    +  if (!eventEmitter) {
    +    return;
    +  }
       eventEmitter->onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{
         .position = static_cast<double>(position),
         .offset = offset
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions