Skip to content

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

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

zewillze wants to merge 1 commit into
callstack:masterfrom
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

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` toggled
`isScrollEnabled` and `bounces` on the collection view only, so on iOS 15 a
pager with `scrollEnabled={false}` could still be swiped even though the prop
did reach the native view (outer `isScrollEnabled` is `false`, the embedded
paging scroll view keeps `isScrollEnabled = true` and an enabled pan gesture).

Apply both values to the embedded paging scroll views as well. The traversal
stops at `UICollectionViewCell` boundaries so scroll views belonging to the
pages' React Native content are never touched. On iOS 16+ the embedded scroll
view does not exist and the collection view itself is the pager, so behaviour
there is unchanged.

This also makes `overdrag` work on iOS 15 for the same reason: `bounces` was
only applied to the collection view, leaving over-drag enabled.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant