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
1 change: 1 addition & 0 deletions ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ let package = Package(
"Extensions.swift",
"PagerGestureObserver.swift",
"PagerScrollDelegate.swift",
"PagerScrollDelegateIOS15.swift",
"PagerView.swift",
"PagerViewIOS15Compat.swift",
"PagerViewProps.swift",
Expand Down
16 changes: 16 additions & 0 deletions ios/PagerScrollDelegateIOS15.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import UIKit

/// iOS 15 only, see `PagerViewIOS15Compat`. Delete once the minimum supported iOS version is 16.
///
/// The scroll views that page on iOS 15 only exist once the collection view displays its pages, so
/// a `PagerScrollDelegate` is attached to them from here.
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)
}
}
8 changes: 7 additions & 1 deletion ios/PagerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import react_native_pager_view

struct PagerView: View {
@ObservedObject var props: PagerViewProps
@State private var scrollDelegate = PagerScrollDelegate()
@State private var scrollDelegate: PagerScrollDelegate = {
if #available(iOS 16, *) {
return PagerScrollDelegate()
}
// iOS 15 only, see PagerViewIOS15Compat.swift (delete together with the iOS 15 support).
return PagerScrollDelegateIOS15()
}()
@State private var gestureObserver = PagerGestureObserver()
@State private var didEmitInitialPageSelection = false
weak var delegate: PagerViewProviderDelegate?
Expand Down
59 changes: 57 additions & 2 deletions ios/PagerViewIOS15Compat.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import UIKit
#if SWIFT_PACKAGE
import react_native_pager_view
#endif

/// Workarounds for the SwiftUI paged `TabView` on iOS 15.
///
Expand All @@ -8,14 +11,20 @@ import UIKit
/// 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, and the back-gesture arbitration of `PagerGestureObserver`
/// never reached the pan that blocks it.
/// never reached the pan that blocks it. 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 `PagerViewIOS15Compat` calls in `PagerView.swift`
/// - Note: Delete this file and `PagerScrollDelegateIOS15.swift` together with the
/// `PagerViewIOS15Compat` calls and the `PagerScrollDelegateIOS15` choice in `PagerView.swift`
/// once the minimum supported iOS version is 16.
enum PagerViewIOS15Compat {
private static var scrollDelegateKey: UInt8 = 0
private static var originalDelegateKey: 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 Down Expand Up @@ -47,6 +56,33 @@ enum PagerViewIOS15Compat {
pagingScrollViews(in: collectionView).forEach { gestureObserver.attach(to: $0) }
}

/// 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) {
if scrollView.delegate is PagerScrollDelegate {
continue
}

let originalDelegate = ScrollViewDelegateForwarder(target: scrollView.delegate)
let scrollDelegate = PagerScrollDelegate()
scrollDelegate.originalDelegate = originalDelegate
scrollDelegate.delegate = delegate
// Both delegate references are weak; the scroll view owns them instead.
let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(scrollView, &scrollDelegateKey, scrollDelegate, policy)
objc_setAssociatedObject(scrollView, &originalDelegateKey, originalDelegate, policy)
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 Expand Up @@ -75,3 +111,22 @@ enum PagerViewIOS15Compat {
return scrollViews
}
}

/// `PagerScrollDelegate` forwards to a `UICollectionViewDelegate`, but the embedded scroll views'
/// own delegate (UIKit's `_UICollectionViewOrthogonalScrollerSectionController`) is only a
/// `UIScrollViewDelegate`. This passes every call through to it.
private final class ScrollViewDelegateForwarder: NSObject, UICollectionViewDelegate {
private weak var target: UIScrollViewDelegate?

init(target: UIScrollViewDelegate?) {
self.target = target
}

override func responds(to aSelector: Selector!) -> Bool {
target?.responds(to: aSelector) ?? false
}

override func forwardingTarget(for aSelector: Selector!) -> Any? {
target
}
}
Loading