Skip to content
Draft
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: 6 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- react-native-pager-view (6.8.1):
- react-native-pager-view (7.0.0):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1355,6 +1355,7 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SwiftUIIntrospect (~> 1.0)
- Yoga
- react-native-safe-area-context (5.4.0):
- DoubleConversion
Expand Down Expand Up @@ -2032,6 +2033,7 @@ PODS:
- ReactCommon/turbomodule/core
- Yoga
- SocketRocket (0.7.1)
- SwiftUIIntrospect (1.3.0)
- Yoga (0.0.0)

DEPENDENCIES:
Expand Down Expand Up @@ -2120,6 +2122,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- SocketRocket
- SwiftUIIntrospect

EXTERNAL SOURCES:
boost:
Expand Down Expand Up @@ -2321,7 +2324,7 @@ SPEC CHECKSUMS:
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
react-native-pager-view: 919534782a0489f7e2aeeb9a8b8959edfd3f067a
react-native-pager-view: 3bdf418f13ca0eb979c2720b8991a5f46f59386e
react-native-safe-area-context: 562163222d999b79a51577eda2ea8ad2c32b4d06
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
Expand Down Expand Up @@ -2362,6 +2365,7 @@ SPEC CHECKSUMS:
RNScreens: 5621e3ad5a329fbd16de683344ac5af4192b40d3
RNSVG: 8a1054afe490b5d63b9792d7ae3c1fde8c05cdd0
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf

PODFILE CHECKSUM: c21f5b764d10fb848650e6ae2ea533b823c1f648
Expand Down
39 changes: 39 additions & 0 deletions ios/Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation
import SwiftUI
import UIKit

/**
Helper used to render UIView inside of SwiftUI.
*/
struct RepresentableView: UIViewRepresentable {
var view: UIView

// Adding a wrapper UIView to avoid SwiftUI directly managing React Native views.
// This fixes issues with incorrect layout rendering.
func makeUIView(context: Context) -> UIView {
let wrapper = UIView()
wrapper.addSubview(view)
return wrapper
}

func updateUIView(_ uiView: UIView, context: Context) {}
}

extension Collection {
// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

extension UIView {
func pinEdges(to other: UIView) {
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: other.leadingAnchor),
trailingAnchor.constraint(equalTo: other.trailingAnchor),
topAnchor.constraint(equalTo: other.topAnchor),
bottomAnchor.constraint(equalTo: other.bottomAnchor)
])
}
}

98 changes: 98 additions & 0 deletions ios/PagerScrollDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import UIKit

/**
Scroll delegate used to control underlying TabView's collection view.
*/
class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDelegate {
// Store the original delegate to forward calls
weak var originalDelegate: UICollectionViewDelegate?
weak var delegate: PagerViewProviderDelegate?
var orientation: UICollectionView.ScrollDirection = .horizontal

func scrollViewDidScroll(_ scrollView: UIScrollView) {
let isHorizontal = orientation == .horizontal
let pageSize = isHorizontal ? scrollView.frame.width : scrollView.frame.height
let contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y

guard pageSize > 0 else { return }

let offset = contentOffset.truncatingRemainder(dividingBy: pageSize) / pageSize
let position = round(contentOffset / pageSize - offset)

let eventData = OnPageScrollEventData(position: position, offset: offset)
delegate?.onPageScroll(data: eventData)
originalDelegate?.scrollViewDidScroll?(scrollView)
}

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
delegate?.onPageScrollStateChanged(state: .dragging)
originalDelegate?.scrollViewWillBeginDragging?(scrollView)
}

func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
delegate?.onPageScrollStateChanged(state: .settling)
originalDelegate?.scrollViewWillBeginDecelerating?(scrollView)
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
delegate?.onPageScrollStateChanged(state: .idle)
originalDelegate?.scrollViewDidEndDecelerating?(scrollView)
}

func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
delegate?.onPageScrollStateChanged(state: .idle)
originalDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
delegate?.onPageScrollStateChanged(state: .idle)
}
originalDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
}

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
originalDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
originalDelegate?.collectionView?(collectionView, willDisplay: cell, forItemAt: indexPath)
}

override func responds(to aSelector: Selector!) -> Bool {
let handledSelectors: [Selector] = [
#selector(scrollViewDidScroll(_:)),
#selector(scrollViewWillBeginDragging(_:)),
#selector(scrollViewWillBeginDecelerating(_:)),
#selector(scrollViewDidEndDecelerating(_:)),
#selector(scrollViewDidEndScrollingAnimation(_:)),
#selector(scrollViewDidEndDragging(_:willDecelerate:)),
#selector(collectionView(_:didEndDisplaying:forItemAt:)),
#selector(collectionView(_:willDisplay:forItemAt:))
]

if handledSelectors.contains(aSelector) {
return true
}
return originalDelegate?.responds(to: aSelector) ?? false
}

override func forwardingTarget(for aSelector: Selector!) -> Any? {
let handledSelectors: [Selector] = [
#selector(scrollViewDidScroll(_:)),
#selector(scrollViewWillBeginDragging(_:)),
#selector(scrollViewWillBeginDecelerating(_:)),
#selector(scrollViewDidEndDecelerating(_:)),
#selector(scrollViewDidEndScrollingAnimation(_:)),
#selector(scrollViewDidEndDragging(_:willDecelerate:)),
#selector(collectionView(_:didEndDisplaying:forItemAt:)),
#selector(collectionView(_:willDisplay:forItemAt:))
]

if handledSelectors.contains(aSelector) {
return nil
}
return originalDelegate
}
}

61 changes: 61 additions & 0 deletions ios/PagerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect

struct PagerView: View {
@ObservedObject var props: PagerViewProps
@State private var scrollDelegate = PagerScrollDelegate()
weak var delegate: PagerViewProviderDelegate?

@Weak var collectionView: UICollectionView?

var body: some View {
TabView(selection: $props.currentPage) {
ForEach(props.children) { child in
if let index = props.children.firstIndex(of: child) {
RepresentableView(view: child.view)
.ignoresSafeArea(.container, edges: .vertical)
.tag(index)
}
}
}
.id(props.children.count)
.background(.clear)
.tabViewStyle(.page(indexDisplayMode: .never))
.ignoresSafeArea(.all, edges: .all)
.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
collectionView.keyboardDismissMode = props.keyboardDismissMode

if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = props.orientation
}

if scrollDelegate.originalDelegate == nil {
scrollDelegate.originalDelegate = collectionView.delegate
scrollDelegate.delegate = delegate
scrollDelegate.orientation = props.orientation
collectionView.delegate = scrollDelegate
}
}
.onChange(of: props.children) { newValue in
if props.currentPage >= newValue.count && !newValue.isEmpty {
props.currentPage = newValue.count - 1
}
}
.onChange(of: props.currentPage) { newValue in
delegate?.onPageSelected(position: newValue)
}
.onChange(of: props.scrollEnabled) { newValue in
collectionView?.isScrollEnabled = newValue
}
.onChange(of: props.overdrag) { newValue in
collectionView?.bounces = newValue
}
.onChange(of: props.keyboardDismissMode) { newValue in
collectionView?.keyboardDismissMode = newValue
}
}
}
35 changes: 35 additions & 0 deletions ios/PagerViewProps.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SwiftUI
import UIKit

struct IdentifiablePlatformView: Identifiable, Equatable {
let id = UUID()
let view: UIView

init(_ view: UIView) {
self.view = view
}
}

@objc public enum PagerLayoutDirection: Int {
case ltr
case rtl

var converted: LayoutDirection {
switch self {
case .ltr:
return .leftToRight
case .rtl:
return .rightToLeft
}
}
}

class PagerViewProps: ObservableObject {
@Published var children: [IdentifiablePlatformView] = []
@Published var currentPage: Int = -1
@Published var scrollEnabled: Bool = true
@Published var overdrag: Bool = false
@Published var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none
@Published var layoutDirection: PagerLayoutDirection = .ltr
@Published var orientation: UICollectionView.ScrollDirection = .horizontal
}
Loading
Loading