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 attaches a PagerScrollDelegate to the embedded scroll view too, from collectionView(_:willDisplay:forItemAt:) since it doesn't exist yet when introspect runs. The iOS 15 code is in PagerScrollDelegateIOS15.swift and PagerViewIOS15Compat.swift, and PagerScrollDelegate.swift is unchanged.

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".
  • Issue [iOS] react-native-screens v4.19.0 breaks the global back swipe gesture on pager views #1050 Back Gesture Repro: paging works as on main. I couldn't test the back swipe itself, since synthesized edge swipes don't trigger it on the simulator.

e2e on iOS 26.5 passes, apart from flaky tests that pass on re-run. On iOS 17.5 it fails the same tests as main. The SwiftPM example builds too.

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
Comment thread ios/PagerScrollDelegate.swift Outdated
Comment thread ios/PagerScrollDelegate.swift Outdated
@troZee

troZee commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks @d-mato, great fix and a really clear write-up! 🙏

One request: please move all the iOS 15 code into its own file and leave PagerScrollDelegate.swift unchanged.

Why: React Native still supports iOS 15.1 as its minimum (https://github.com/react/react-native/blob/7fc911afd9d4ee46bdeaf9a9ddd29a8366060d8a/packages/react-native/scripts/cocoapods/helpers.rb#L84), so we need this fix for now. But iOS 27 is coming, and we plan to drop these iOS 15 workarounds later. When that happens, removing them should take a couple of minutes: delete the iOS 15 files and update one or two call sites in PagerView.swift, with no hunting through shared code.

What to move: right now these iOS 15 changes are in the delegate that every iOS version uses:

  • the originalDelegate type change (UICollectionViewDelegate? → UIScrollViewDelegate?)
  • the two as? UICollectionViewDelegate casts
  • the attachScrollDelegate(...) call in willDisplay

Suggestion (just a sketch):

// ios/PagerScrollDelegateIOS15.swift
// iOS 15 only. Delete once the minimum supported iOS version is 16.
final class PagerScrollDelegateIOS15: PagerScrollDelegate {
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
PagerViewIOS15Compat.attachScrollDelegate(to: collectionView, delegate: delegate)
super.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath)
}
}

PagerView.swift then chooses this delegate on iOS 15. If the embedded scroll view needs a plain UIScrollViewDelegate to forward to, please handle that in the iOS 15 file as well.

Everything else looks great. Thanks again! 🚀

@d-mato
d-mato force-pushed the fix/ios15-page-scroll-events branch from f1a42fc to 0f76ed7 Compare October 26, 2026 09:53
@d-mato

d-mato commented Oct 26, 2026

Copy link
Copy Markdown
Author

Thanks, done. PagerScrollDelegate.swift is back to main, and the iOS 15 code is in PagerScrollDelegateIOS15.swift (attaching from willDisplay as you suggested) and PagerViewIOS15Compat.swift. Dropping iOS 15 now only needs deleting those two files and their call sites in PagerView.swift.

I also rebased onto main to resolve the conflict with #1195.

@troZee
troZee requested a balanced review from Copilot October 26, 2026 11:40

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

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)

3 participants