Skip to content
Merged
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
24 changes: 18 additions & 6 deletions ios/PagerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ struct PagerView: View {
props.orientation == .vertical
}

private func applyScrollEnabled(_ scrollEnabled: Bool) {
collectionView?.isScrollEnabled = scrollEnabled
// iOS 15 only, see PagerViewIOS15Compat.swift (delete together with the iOS 15 support).
PagerViewIOS15Compat.applyScrollEnabled(scrollEnabled, to: collectionView)
}

private func applyOverdrag(_ overdrag: Bool) {
collectionView?.bounces = overdrag
// iOS 15 only, see PagerViewIOS15Compat.swift (delete together with the iOS 15 support).
PagerViewIOS15Compat.applyOverdrag(overdrag, to: collectionView)
}

var body: some View {
GeometryReader { proxy in
TabView(selection: $props.currentPage) {
Expand All @@ -39,8 +51,8 @@ struct PagerView: View {
.environment(\.layoutDirection, props.layoutDirection.converted)
.introspect(.tabView(style: .page), on: .iOS(.v14...)) { collectionView in
self.collectionView = collectionView
collectionView.bounces = props.overdrag
collectionView.isScrollEnabled = props.scrollEnabled
applyOverdrag(props.overdrag)
applyScrollEnabled(props.scrollEnabled)
collectionView.keyboardDismissMode = props.keyboardDismissMode
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
Expand Down Expand Up @@ -70,8 +82,8 @@ struct PagerView: View {
// Apply initial prop values that .onChange won't catch
// (.onChange only fires on changes, not on initial values)
DispatchQueue.main.async {
collectionView?.isScrollEnabled = props.scrollEnabled
collectionView?.bounces = props.overdrag
applyScrollEnabled(props.scrollEnabled)
applyOverdrag(props.overdrag)
}

// `onChange` does not fire for the initial `currentPage` value. Emit the
Expand All @@ -91,10 +103,10 @@ struct PagerView: View {
delegate?.onPageSelected(position: newValue)
}
.onChange(of: props.scrollEnabled) { newValue in
collectionView?.isScrollEnabled = newValue
applyScrollEnabled(newValue)
}
.onChange(of: props.overdrag) { newValue in
collectionView?.bounces = newValue
applyOverdrag(newValue)
}
.onChange(of: props.keyboardDismissMode) { newValue in
collectionView?.keyboardDismissMode = newValue
Expand Down
63 changes: 63 additions & 0 deletions ios/PagerViewIOS15Compat.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import UIKit

/// Workarounds for the SwiftUI paged `TabView` on iOS 15.
///
/// 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 (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.
///
/// 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.
enum PagerViewIOS15Compat {
/// 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, *) {
return
}

pagingScrollViews(in: collectionView).forEach { $0.isScrollEnabled = scrollEnabled }
}

/// Mirrors `overdrag` onto the scroll views SwiftUI embedded in the paging collection view.
static func applyOverdrag(_ overdrag: Bool, to collectionView: UICollectionView?) {
if #available(iOS 16, *) {
return
}

pagingScrollViews(in: collectionView).forEach { $0.bounces = overdrag }
}

/// 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
/// the traversal stops at cell boundaries and never touches the user's own scroll views.
private static func pagingScrollViews(in collectionView: UICollectionView?) -> [UIScrollView] {
guard let collectionView else { return [] }

return collectPagingScrollViews(in: collectionView)
}

private static func collectPagingScrollViews(in view: UIView) -> [UIScrollView] {
var scrollViews: [UIScrollView] = []

for subview in view.subviews {
if subview is UICollectionViewCell {
continue
}

if let scrollView = subview as? UIScrollView {
scrollViews.append(scrollView)
}

scrollViews.append(contentsOf: collectPagingScrollViews(in: subview))
}

return scrollViews
}
}
Loading