Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ios/PagerScrollDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import UIKit
Scroll delegate used to control underlying TabView's collection view.
*/
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.

weak var delegate: PagerViewProviderDelegate?
var orientation: UICollectionView.ScrollDirection = .horizontal

Expand Down Expand Up @@ -78,11 +78,13 @@ class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDeleg
}

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.

}

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 {
Expand Down
25 changes: 23 additions & 2 deletions ios/PagerViewIOS15Compat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import UIKit
/// 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.
/// and the one in `PagerScrollDelegate.swift` once the minimum supported iOS version is 16.
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, *) {
Expand All @@ -33,6 +37,23 @@ enum PagerViewIOS15Compat {
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) where !(scrollView.delegate is PagerScrollDelegate) {
let scrollDelegate = PagerScrollDelegate()
scrollDelegate.originalDelegate = scrollView.delegate
scrollDelegate.delegate = delegate
// `UIScrollView.delegate` is weak; the scroll view owns its delegate instead.
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
Expand Down
Loading