Skip to content

fix(ios): respect scrollEnabled on iOS 15 - #1148

Open
zewillze wants to merge 1 commit into
callstack:mainfrom
zewillze:fix/ios-scrollenabled-ios15
Open

zewillze wants to merge 1 commit into
callstack:mainfrom
zewillze:fix/ios-scrollenabled-ios15

Conversation

@zewillze

Copy link
Copy Markdown

Summary

Fixes the remaining iOS 15 case of #1028: with scrollEnabled={false} the pager can still be swiped by the user.

Why it only happens on iOS 15. SwiftUI implements .tabViewStyle(.page) with a compositional layout that pages orthogonally. On iOS 15 the actual horizontal paging is performed by an embedded UIScrollView subview of the collection view (_UICollectionViewOrthogonalScrollerEmbeddedScrollView), while the collection view itself only scrolls vertically. PagerView applied isScrollEnabled / bounces only to the collection view, so the outer view was correctly disabled while the view that really handles the swipe stayed enabled. Measured on an iOS 15.2 simulator with scrollEnabled={false}:

view class isScrollEnabled panGestureRecognizer.isEnabled contentSize.width
outer PagingCollectionView (UICollectionView) 0 0 428 (1 page, vertical)
inner _UICollectionViewOrthogonalScrollerEmbeddedScrollView 1 1 856 (2 pages)

PagerViewProvider.scrollEnabled is 0 as well, i.e. the prop does reach the native view — it just never reaches the scroll view that pages. On iOS 16+ the embedded scroll view does not exist and the collection view itself is the pager (contentSize.width = 804 on iOS 18.5), which is why disabling it was enough there.

This is a different failure mode from #1071 (initial-value timing / .onAppear); with 9.0.4 the outer collection view is already disabled, so the iOS 15 report in #1028 is still reproducible.

  • How it was implemented: applyScrollEnabled(_:) / applyOverdrag(_:) now forward the values to the collection view and to the paging scroll views embedded in it. The traversal stops at UICollectionViewCell boundaries, so scroll views belonging to the pages' React Native content (FlatList / ScrollView inside a page) are never touched. All four call sites (introspect, onAppear, and the two onChange handlers) go through the new helpers.
  • Impact: iOS implementation only (scrollEnabled, overdrag). No API, JS or typing change. On iOS versions where no embedded scroll view exists, behaviour is unchanged.
  • While here, overdrag gets the same treatment for the same reason: bounces was only applied to the collection view, so over-drag stayed enabled on iOS 15 even with the default overdrag={false}.

Test Plan

What's required for testing (prerequisites)?

  • An iOS 15.x simulator or device (e.g. iPhone 13 / iOS 15.2). On iOS 16+ the bug does not reproduce, because SwiftUI's page TabView no longer uses an embedded paging scroll view there.
  • bun install && bun bootstrap, then bun example:ios.

What are the steps to reproduce (after prerequisites)?

  1. bun example:ios and open the Basic example.
  2. Tap "Scroll Enabled" so the button reads "Scroll Disabled" (scrollEnabled={false}; example/src/component/NavigationPanel/ControlPanel.tsx).
  3. Swipe horizontally.
    • Before: the page changes.
    • After: the page does not move; page changes via the navigation panel (setPage) still work.

Optional, to check the mechanism without relying on gestures — attach lldb and read the paging scroll views (the embedded one is visible via recursiveDescription):

expr -l objc++ -O -- (BOOL)[(UIScrollView *)$sv isScrollEnabled]
expr -l objc++ -O -- (BOOL)[[(UIScrollView *)$sv panGestureRecognizer] isEnabled]

$sv = _UICollectionViewOrthogonalScrollerEmbeddedScrollView. Before: 1 / 1. After: 0 / 0. (UIScrollView.isScrollEnabled = false also disables its pan gesture recognizer, so nothing can scroll it any more.)

I also confirmed with the same probe, on iOS 15.2 and iOS 18.5:

  • scrollEnabled={true} re-enables both scroll views at runtime (.onChange path) — no regression, the pager is not permanently locked;
  • programmatic paging still works while scrolling is disabled ([RNCPagerViewComponentView setPage:1] moves contentOffset.x to the next page);
  • a ScrollView / FlatList inside a page keeps its own scrolling (the traversal skips UICollectionViewCells).

Compatibility

OS Implemented
iOS ✅
Android ❌

Android is unaffected by this code path and already respects scrollEnabled.

Checklist

  • I have tested this on a device and a simulator
  • I added the documentation in README.md
  • I updated the typed files (TS and Flow)

Refs #1028

@troZee

troZee commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Thank you for your contribution. Is it possible to create a separate file with all of those changes and wrap it with macro like this?

iOS 15 is very old, and we want to remove it in the future

@troZee
troZee requested a balanced review from Copilot October 8, 2026 08:31
@troZee troZee added the ios label Oct 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused implementation addresses the iOS 15 hierarchy without affecting scroll views inside page content.

0 open findings

What changed in this PR

Fixes iOS 15 paging so scrollEnabled and overdrag also apply to SwiftUI’s embedded paging scroll view.

Changes:

  • Discovers paging scroll views while excluding page-content cells.
  • Centralizes scroll and bounce state updates across all lifecycle paths.
File Description
ios/​PagerView.swift Applies scrolling and overdrag settings to outer and embedded paging views.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

On iOS 15 SwiftUI implements `.tabViewStyle(.page)` with a compositional
layout that pages orthogonally: the horizontal paging is performed by an
embedded `UIScrollView` subview of the collection view
(`_UICollectionViewOrthogonalScrollerEmbeddedScrollView`), while the
collection view itself only scrolls vertically. `PagerView` can only toggle
the collection view, so on iOS 15 a pager with `scrollEnabled={false}` could
still be swiped even though the prop reached the native view (the outer
`isScrollEnabled` is `false` while the embedded paging scroll view keeps
`isScrollEnabled = true` and an enabled pan gesture). `overdrag` had the same
problem: `bounces` never reached the view that actually scrolls.

Mirror both values onto the scroll views SwiftUI embedded in the paging
collection view. The traversal stops at `UICollectionViewCell` boundaries so
scroll views belonging to the pages' React Native content are never touched.

On iOS 16 and later the collection view itself is the pager and none of this
is needed, so the whole workaround is isolated in the new
`PagerViewIOS15Compat.swift` and every entry point starts with
`if #available(iOS 16, *) { return }`. Deleting that file and its two call
sites in `PagerView.swift` is all that is required once iOS 15 support is
dropped.

Refs callstack#1028
@zewillze
zewillze force-pushed the fix/ios-scrollenabled-ios15 branch from 227b0af to 18c93fa Compare October 8, 2026 14:30
@zewillze

zewillze commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thank you for your contribution. Is it possible to create a separate file with all of those changes and wrap it with macro like this?

iOS 15 is very old, and we want to remove it in the future

Done — the iOS 15 part is now isolated and gated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants