Skip to content

Commit 1b89f44

Browse files
committed
feat: rewrite to SwiftUI
1 parent 892c36b commit 1b89f44

11 files changed

+478
-378
lines changed

example/ios/Podfile.lock

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,7 @@ PODS:
13321332
- React-jsiexecutor
13331333
- React-RCTFBReactNativeSpec
13341334
- ReactCommon/turbomodule/core
1335-
- react-native-pager-view (6.8.1):
1335+
- react-native-pager-view (7.0.0):
13361336
- DoubleConversion
13371337
- glog
13381338
- hermes-engine
@@ -1355,6 +1355,7 @@ PODS:
13551355
- ReactCodegen
13561356
- ReactCommon/turbomodule/bridging
13571357
- ReactCommon/turbomodule/core
1358+
- SwiftUIIntrospect (~> 1.0)
13581359
- Yoga
13591360
- react-native-safe-area-context (5.4.0):
13601361
- DoubleConversion
@@ -2032,6 +2033,7 @@ PODS:
20322033
- ReactCommon/turbomodule/core
20332034
- Yoga
20342035
- SocketRocket (0.7.1)
2036+
- SwiftUIIntrospect (1.3.0)
20352037
- Yoga (0.0.0)
20362038

20372039
DEPENDENCIES:
@@ -2120,6 +2122,7 @@ DEPENDENCIES:
21202122
SPEC REPOS:
21212123
trunk:
21222124
- SocketRocket
2125+
- SwiftUIIntrospect
21232126

21242127
EXTERNAL SOURCES:
21252128
boost:
@@ -2321,7 +2324,7 @@ SPEC CHECKSUMS:
23212324
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
23222325
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
23232326
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
2324-
react-native-pager-view: 919534782a0489f7e2aeeb9a8b8959edfd3f067a
2327+
react-native-pager-view: 3bdf418f13ca0eb979c2720b8991a5f46f59386e
23252328
react-native-safe-area-context: 562163222d999b79a51577eda2ea8ad2c32b4d06
23262329
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
23272330
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
@@ -2362,6 +2365,7 @@ SPEC CHECKSUMS:
23622365
RNScreens: 5621e3ad5a329fbd16de683344ac5af4192b40d3
23632366
RNSVG: 8a1054afe490b5d63b9792d7ae3c1fde8c05cdd0
23642367
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2368+
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
23652369
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
23662370

23672371
PODFILE CHECKSUM: c21f5b764d10fb848650e6ae2ea533b823c1f648

ios/Extensions.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Foundation
2+
import SwiftUI
3+
import UIKit
4+
5+
/**
6+
Helper used to render UIView inside of SwiftUI.
7+
*/
8+
struct RepresentableView: UIViewRepresentable {
9+
var view: UIView
10+
11+
func makeUIView(context: Context) -> UIView {
12+
let wrapper = UIView()
13+
wrapper.addSubview(view)
14+
return wrapper
15+
}
16+
17+
func updateUIView(_ uiView: UIView, context: Context) {}
18+
}
19+
20+
extension Collection {
21+
// Returns the element at the specified index if it is within bounds, otherwise nil.
22+
subscript(safe index: Index) -> Element? {
23+
indices.contains(index) ? self[index] : nil
24+
}
25+
}
26+
27+
extension UIView {
28+
func pinEdges(to other: UIView) {
29+
NSLayoutConstraint.activate([
30+
leadingAnchor.constraint(equalTo: other.leadingAnchor),
31+
trailingAnchor.constraint(equalTo: other.trailingAnchor),
32+
topAnchor.constraint(equalTo: other.topAnchor),
33+
bottomAnchor.constraint(equalTo: other.bottomAnchor)
34+
])
35+
}
36+
}
37+

ios/PagerScrollDelegate.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import UIKit
2+
3+
/**
4+
Scroll delegate used to control underlying TabView's collection view.
5+
*/
6+
class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDelegate {
7+
// Store the original delegate to forward calls
8+
weak var originalDelegate: UICollectionViewDelegate?
9+
weak var delegate: PagerViewProviderDelegate?
10+
var orientation: UICollectionView.ScrollDirection = .horizontal
11+
12+
func scrollViewDidScroll(_ scrollView: UIScrollView) {
13+
let isHorizontal = orientation == .horizontal
14+
let pageSize = isHorizontal ? scrollView.frame.width : scrollView.frame.height
15+
let contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y
16+
17+
guard pageSize > 0 else { return }
18+
19+
let offset = contentOffset.truncatingRemainder(dividingBy: pageSize) / pageSize
20+
let position = round(contentOffset / pageSize - offset)
21+
22+
let eventData = OnPageScrollEventData(position: position, offset: offset)
23+
delegate?.onPageScroll(data: eventData)
24+
originalDelegate?.scrollViewDidScroll?(scrollView)
25+
}
26+
27+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
28+
delegate?.onPageScrollStateChanged(state: .dragging)
29+
originalDelegate?.scrollViewWillBeginDragging?(scrollView)
30+
}
31+
32+
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
33+
delegate?.onPageScrollStateChanged(state: .settling)
34+
originalDelegate?.scrollViewWillBeginDecelerating?(scrollView)
35+
}
36+
37+
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
38+
delegate?.onPageScrollStateChanged(state: .idle)
39+
originalDelegate?.scrollViewDidEndDecelerating?(scrollView)
40+
}
41+
42+
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
43+
delegate?.onPageScrollStateChanged(state: .idle)
44+
originalDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
45+
}
46+
47+
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
48+
if !decelerate {
49+
delegate?.onPageScrollStateChanged(state: .idle)
50+
}
51+
originalDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
52+
}
53+
54+
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
55+
originalDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
56+
}
57+
58+
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
59+
originalDelegate?.collectionView?(collectionView, willDisplay: cell, forItemAt: indexPath)
60+
}
61+
62+
override func responds(to aSelector: Selector!) -> Bool {
63+
let handledSelectors: [Selector] = [
64+
#selector(scrollViewDidScroll(_:)),
65+
#selector(scrollViewWillBeginDragging(_:)),
66+
#selector(scrollViewWillBeginDecelerating(_:)),
67+
#selector(scrollViewDidEndDecelerating(_:)),
68+
#selector(scrollViewDidEndScrollingAnimation(_:)),
69+
#selector(scrollViewDidEndDragging(_:willDecelerate:)),
70+
#selector(collectionView(_:didEndDisplaying:forItemAt:)),
71+
#selector(collectionView(_:willDisplay:forItemAt:))
72+
]
73+
74+
if handledSelectors.contains(aSelector) {
75+
return true
76+
}
77+
return originalDelegate?.responds(to: aSelector) ?? false
78+
}
79+
80+
override func forwardingTarget(for aSelector: Selector!) -> Any? {
81+
let handledSelectors: [Selector] = [
82+
#selector(scrollViewDidScroll(_:)),
83+
#selector(scrollViewWillBeginDragging(_:)),
84+
#selector(scrollViewWillBeginDecelerating(_:)),
85+
#selector(scrollViewDidEndDecelerating(_:)),
86+
#selector(scrollViewDidEndScrollingAnimation(_:)),
87+
#selector(scrollViewDidEndDragging(_:willDecelerate:)),
88+
#selector(collectionView(_:didEndDisplaying:forItemAt:)),
89+
#selector(collectionView(_:willDisplay:forItemAt:))
90+
]
91+
92+
if handledSelectors.contains(aSelector) {
93+
return nil
94+
}
95+
return originalDelegate
96+
}
97+
}
98+

ios/PagerView.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import SwiftUI
2+
@_spi(Advanced) import SwiftUIIntrospect
3+
4+
struct PagerView: View {
5+
@ObservedObject var props: PagerViewProps
6+
@State private var scrollDelegate = PagerScrollDelegate()
7+
weak var delegate: PagerViewProviderDelegate?
8+
9+
@Weak var collectionView: UICollectionView?
10+
11+
var body: some View {
12+
TabView(selection: $props.currentPage) {
13+
ForEach(props.children, id: \.tag) { child in
14+
let index = props.children.firstIndex(of: child) ?? 0
15+
16+
RepresentableView(view: child)
17+
.ignoresSafeArea(.container, edges: .vertical)
18+
.tag(index)
19+
}
20+
}
21+
.id(props.children.count)
22+
.background(.clear)
23+
.tabViewStyle(.page(indexDisplayMode: .never))
24+
.ignoresSafeArea(.all, edges: .all)
25+
.environment(\.layoutDirection, props.layoutDirection.converted)
26+
.introspect(.tabView(style: .page), on: .iOS(.v14...)) { collectionView in
27+
self.collectionView = collectionView
28+
collectionView.bounces = props.overdrag
29+
collectionView.isScrollEnabled = props.scrollEnabled
30+
collectionView.keyboardDismissMode = props.keyboardDismissMode
31+
32+
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
33+
layout.scrollDirection = props.orientation
34+
}
35+
36+
if scrollDelegate.originalDelegate == nil {
37+
scrollDelegate.originalDelegate = collectionView.delegate
38+
scrollDelegate.delegate = delegate
39+
scrollDelegate.orientation = props.orientation
40+
collectionView.delegate = scrollDelegate
41+
}
42+
}
43+
.onChange(of: props.children) { newValue in
44+
if props.currentPage >= newValue.count && !newValue.isEmpty {
45+
props.currentPage = newValue.count - 1
46+
}
47+
}
48+
.onChange(of: props.currentPage) { newValue in
49+
delegate?.onPageSelected(position: newValue)
50+
}
51+
.onChange(of: props.scrollEnabled) { newValue in
52+
collectionView?.isScrollEnabled = newValue
53+
}
54+
.onChange(of: props.overdrag) { newValue in
55+
collectionView?.bounces = newValue
56+
}
57+
.onChange(of: props.keyboardDismissMode) { newValue in
58+
collectionView?.keyboardDismissMode = newValue
59+
}
60+
}
61+
}

ios/PagerViewProps.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
@objc public enum PagerLayoutDirection: Int {
5+
case ltr
6+
case rtl
7+
8+
var converted: LayoutDirection {
9+
switch self {
10+
case .ltr:
11+
return .leftToRight
12+
case .rtl:
13+
return .rightToLeft
14+
}
15+
}
16+
}
17+
18+
class PagerViewProps: ObservableObject {
19+
@Published var children: [UIView] = []
20+
@Published var currentPage: Int = -1
21+
@Published var scrollEnabled: Bool = true
22+
@Published var overdrag: Bool = false
23+
@Published var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none
24+
@Published var layoutDirection: PagerLayoutDirection = .ltr
25+
@Published var orientation: UICollectionView.ScrollDirection = .horizontal
26+
}

ios/PagerViewProvider.swift

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
5+
6+
@objc public enum PageScrollState: Int {
7+
case idle
8+
case dragging
9+
case settling
10+
}
11+
12+
@objcMembers public class OnPageScrollEventData: NSObject {
13+
public let position: Double
14+
public let offset: Double
15+
16+
init(position: Double, offset: Double) {
17+
self.position = position
18+
self.offset = offset
19+
super.init()
20+
}
21+
}
22+
23+
@objc public protocol PagerViewProviderDelegate {
24+
func onPageScroll(data: OnPageScrollEventData)
25+
func onPageScrollStateChanged(state: PageScrollState)
26+
func onPageSelected(position: Int)
27+
}
28+
29+
@objc public class PagerViewProvider: UIView {
30+
private weak var delegate: PagerViewProviderDelegate?
31+
private var hostingController: UIHostingController<PagerView>?
32+
private var props = PagerViewProps()
33+
34+
@objc public var scrollEnabled: Bool = true {
35+
didSet {
36+
props.scrollEnabled = scrollEnabled
37+
}
38+
}
39+
40+
@objc public var overdrag: Bool = false {
41+
didSet {
42+
props.overdrag = overdrag
43+
}
44+
}
45+
46+
@objc public var currentPage: Int = -1 {
47+
didSet {
48+
props.currentPage = currentPage
49+
}
50+
}
51+
@objc public var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none {
52+
didSet {
53+
props.keyboardDismissMode = keyboardDismissMode
54+
}
55+
}
56+
57+
@objc public var layoutDirection: PagerLayoutDirection = .ltr {
58+
didSet {
59+
props.layoutDirection = layoutDirection
60+
}
61+
}
62+
@objc public var orientation: UICollectionView.ScrollDirection = .horizontal {
63+
didSet {
64+
props.orientation = orientation
65+
}
66+
}
67+
68+
@objc public convenience init(delegate: PagerViewProviderDelegate) {
69+
self.init()
70+
self.delegate = delegate
71+
}
72+
73+
override public func didUpdateReactSubviews() {
74+
props.children = reactSubviews()
75+
}
76+
77+
@objc(insertChild:atIndex:)
78+
public func insertChild(_ child: UIView, at index: Int) {
79+
guard index >= 0 && index <= props.children.count else {
80+
return
81+
}
82+
props.children.insert(child, at: index)
83+
}
84+
85+
@objc(removeChildAtIndex:)
86+
public func removeChild(at index: Int) {
87+
guard index >= 0 && index < props.children.count else {
88+
return
89+
}
90+
props.children.remove(at: index)
91+
}
92+
93+
override public func layoutSubviews() {
94+
super.layoutSubviews()
95+
setupView()
96+
}
97+
98+
@objc public func goTo(index: Int, animated: Bool) {
99+
if animated {
100+
withAnimation {
101+
props.currentPage = index
102+
}
103+
} else {
104+
props.currentPage = index
105+
}
106+
}
107+
108+
private func setupView() {
109+
if self.hostingController != nil {
110+
return
111+
}
112+
113+
self.hostingController = UIHostingController(rootView: PagerView(props: props, delegate: delegate))
114+
if let hostingController = self.hostingController, let parentViewController = reactViewController() {
115+
parentViewController.addChild(hostingController)
116+
hostingController.view.backgroundColor = .clear
117+
addSubview(hostingController.view)
118+
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
119+
hostingController.view.pinEdges(to: self)
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)