Skip to content

fix(ios): emit onPageScroll and onPageScrollStateChanged on iOS 15 - #1179

Open
d-mato wants to merge 1 commit into
callstack:mainfrom
d-mato:fix/ios15-page-scroll-events
Open

d-mato wants to merge 1 commit into
callstack:mainfrom
d-mato:fix/ios15-page-scroll-events

Conversation

@d-mato

@d-mato d-mato commented Oct 14, 2026 •

Copy link
Copy Markdown

Summary

Fixes #1080.

On iOS 15, onPageScroll and onPageScrollStateChanged are never emitted, so the tab indicator of react-native-tab-view / material-top-tabs doesn't follow the page. onPageSelected still works.

Same root cause as #1148: on iOS 15 the paging is done by a scroll view SwiftUI embeds in the collection view (_UICollectionViewOrthogonalScrollerEmbeddedScrollView), and PagerScrollDelegate is only set on the collection view, so it never sees the scroll.

This puts a PagerScrollDelegate in front of the embedded scroll view's delegate as well, forwarding to the original one. The embedded scroll view doesn't exist yet when introspect runs, so it's attached from collectionView(_:willDisplay:forItemAt:). It's a no-op on iOS 16+, like the rest of PagerViewIOS15Compat.

Before:

ios15-material-top-tabs-before.mp4

After:

ios15-material-top-tabs-after.mp4

Test Plan

Example app on an iOS 15.5 simulator (iPad Air 2):

  • MaterialTopBarExample: tap Login, then swipe to Tab2. Before, the indicator stays on Tab1.
  • OnPageScroll Example: swipe or tap a page. Before, the progress bar stays at 0.
  • Basic Example and the vertical one: after a swipe, the Logs tab shows State: dragging, scroll and State: idle. Still works after "Add new page".

The e2e suite passes on iOS 26.5.

We also applied this as a patch to 8.0.5 in our app, and the tabs of both react-native-tab-view and react-native-collapsible-tab-view work again on iOS 15.5.

Compatibility

OS Implemented
iOS ✅
Android ❌

Checklist

  • I have tested this on a device and a simulator (simulator only, I don't have an iOS 15 device)
  • I added the documentation in README.md
  • I updated the typed files (TS and Flow)

@d-mato
d-mato marked this pull request as ready for review October 14, 2026 05:03
*/
class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDelegate {
weak var originalDelegate: UICollectionViewDelegate?
weak var originalDelegate: UIScrollViewDelegate?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

q: Could you explain this change?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

On iOS 15 the delegate of the embedded scroll view is UIKit's private _UICollectionViewOrthogonalScrollerSectionController. It only conforms to UIScrollViewDelegate, not UICollectionViewDelegate, and PagerScrollDelegate now forwards to it too.


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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

q: Why is this casting needed? Why can't we have weak var originalDelegate: UICollectionViewDelegate?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Because of the type change on line 7. With UICollectionViewDelegate? we'd have to store scrollView.delegate as? UICollectionViewDelegate, which is nil for it, so its scroll callbacks would stop being forwarded.

The cast always succeeds here. These two methods are only called on the collection view's instance, and the original delegate there is SwiftUI's coordinator, which conforms to UICollectionViewDelegate. I checked this in lldb on iOS 15.5 and 26.5.

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.

[iOS 15] Tab indicator does not update when switching pages after SwiftUI migration (v8)

2 participants