From 9ecf5abfaa47f922112aed8b92a587f7ba3f8706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 20 Aug 2025 13:20:08 +0200 Subject: [PATCH 1/5] feat: rewrite to SwiftUI --- bun.lockb | Bin 516699 -> 526499 bytes example/ios/Podfile.lock | 8 +- ios/Extensions.swift | 39 +++ ios/PagerScrollDelegate.swift | 98 +++++++ ios/PagerView.swift | 61 ++++ ios/PagerViewProps.swift | 35 +++ ios/PagerViewProvider.swift | 122 ++++++++ ios/RNCPagerViewComponentView.h | 2 + ios/RNCPagerViewComponentView.mm | 470 +++++++------------------------ react-native-pager-view.podspec | 4 +- src/PagerView.tsx | 38 ++- 11 files changed, 488 insertions(+), 389 deletions(-) create mode 100644 ios/Extensions.swift create mode 100644 ios/PagerScrollDelegate.swift create mode 100644 ios/PagerView.swift create mode 100644 ios/PagerViewProps.swift create mode 100644 ios/PagerViewProvider.swift diff --git a/bun.lockb b/bun.lockb index 60c8294785ca096961051029d715c03d08132a36..6fc95a6a7e3dc688188dadc28bcb46aa537cf3f2 100755 GIT binary patch delta 16146 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 Gcm5A$qCfxu delta 14820 zcmZWw3z%C)^}ly_(Y$p3fd%)NK6?)T-p^P4$y z=FB-~&YYQhcf;EFt55Gy{<7JzcE7zlf4O!1+PV95?6>LR!tn7<@r1CGIw2Z5&M=HF z=izzNof1$dkSUh;nRc|xFj9t5G-dlo$L}Y5#AYYuGdnx#w2$9kmaAI1Jv(Z4I6s(o zu#=naKY* zj}kj2YIb_H*;ANz^%U;OwrGQ zNKT?AORna|&aj;Rvur2zE%s2>d+A#c<8s?+s#??OS!_E~vJ;)~dA5_yx(2UiS^Rc3 zDeA|OHW#|ODlXK`)mUiBuGX1u*UXvhx?ySP3hF0G@mgoHChW7ge@Lu9(6pVUXLW1# zuA`BBk^MEi2vWS)6P;ohNvH+&SP*2dp+#hBDvd582Z=b-&hA#Ooj)5sdUmgLwoaN} zbT`B3Ra*5utN$GGC}L4Q2U0J!|SYcCK&WR4(b{mqeV^Iq7E7ZJIgV zOJj?5PtGo8p4C8Ydoii6H`I-(Z6_60JkPauLzY~7TIYKTKHrz3az1lyou3rN)8OcP z8phW!SBgm-QwkTbE2{_qM5|Eg_d>N|r1a?vC0~(^z~1XF^rtm?p&s%Q7x}~Iwes*q zQPXmszo^gL(Oanf-*zWcMu{ypf)WRzcwU9I#d!`mwLE*>SYFp$>SDyI!ywb@izhm< zLeKbwVH}1zI`#QUCw7TT+Fly5&~lJ+l#tW_#po(X#yJNJ&Tu}ugu`d@QmUaykzEI! zP2=D|dg18XxK!7Ybs4W9)>-B<)_4WA6j^y)wF1aqg0;6KB^Q1j<&h;R5&j~aJ$wl854+%_}XZbtQ!$cew0T zmQ%Qj*BjHcuTslkU=aex)!m3sE^+Z{)?VXkeVtp&*zTESx}#MBs0D;LU_|7M?Q5du zt{&IIHF{e;fWp^CL%VttPXom~G<2JkSyR!n7dAwuoggL0hGF4m$&4h^$*x#?9}AmonmXr zOgd9;=$5(16seKy4dgbio~t+LMr#96L*u zb){}kgz3&`$sLW_rmglO`)b`h!>f}{Wm}|smSJ23fxQfdOxM4L9jd%0Dnl#JVq^`a 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 jV}E{J*zE2o#p=r^=^)K diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ea0c7050..f284e1c3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1332,7 +1332,7 @@ PODS: - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - react-native-pager-view (7.0.0): + - react-native-pager-view (7.0.2): - DoubleConversion - glog - hermes-engine @@ -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 @@ -2032,6 +2033,7 @@ PODS: - ReactCommon/turbomodule/core - Yoga - SocketRocket (0.7.1) + - SwiftUIIntrospect (1.3.0) - Yoga (0.0.0) DEPENDENCIES: @@ -2120,6 +2122,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - SocketRocket + - SwiftUIIntrospect EXTERNAL SOURCES: boost: @@ -2321,7 +2324,7 @@ SPEC CHECKSUMS: React-logger: 8edfcedc100544791cd82692ca5a574240a16219 React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468 React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6 - react-native-pager-view: 39dffe42e6c5d419a16e3b8fe6522e43abcdf7e3 + react-native-pager-view: 52b8363d55d54603806f0ac4149783ee11f2f2ce react-native-safe-area-context: 562163222d999b79a51577eda2ea8ad2c32b4d06 React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c @@ -2362,6 +2365,7 @@ SPEC CHECKSUMS: RNScreens: 5621e3ad5a329fbd16de683344ac5af4192b40d3 RNSVG: 8a1054afe490b5d63b9792d7ae3c1fde8c05cdd0 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf PODFILE CHECKSUM: c21f5b764d10fb848650e6ae2ea533b823c1f648 diff --git a/ios/Extensions.swift b/ios/Extensions.swift new file mode 100644 index 00000000..0fb0d1ed --- /dev/null +++ b/ios/Extensions.swift @@ -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) + ]) + } +} + diff --git a/ios/PagerScrollDelegate.swift b/ios/PagerScrollDelegate.swift new file mode 100644 index 00000000..183a8c80 --- /dev/null +++ b/ios/PagerScrollDelegate.swift @@ -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 + } +} + diff --git a/ios/PagerView.swift b/ios/PagerView.swift new file mode 100644 index 00000000..4028a3e1 --- /dev/null +++ b/ios/PagerView.swift @@ -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 + } + } +} diff --git a/ios/PagerViewProps.swift b/ios/PagerViewProps.swift new file mode 100644 index 00000000..fbc2c9ac --- /dev/null +++ b/ios/PagerViewProps.swift @@ -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 +} diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift new file mode 100644 index 00000000..50b85f32 --- /dev/null +++ b/ios/PagerViewProvider.swift @@ -0,0 +1,122 @@ +import SwiftUI +import UIKit + + + +@objc public enum PageScrollState: Int { + case idle + case dragging + case settling +} + +@objcMembers public class OnPageScrollEventData: NSObject { + public let position: Double + public let offset: Double + + init(position: Double, offset: Double) { + self.position = position + self.offset = offset + super.init() + } +} + +@objc public protocol PagerViewProviderDelegate { + func onPageScroll(data: OnPageScrollEventData) + func onPageScrollStateChanged(state: PageScrollState) + func onPageSelected(position: Int) +} + +@objc public class PagerViewProvider: UIView { + private weak var delegate: PagerViewProviderDelegate? + private var hostingController: UIHostingController? + private var props = PagerViewProps() + + @objc public var scrollEnabled: Bool = true { + didSet { + props.scrollEnabled = scrollEnabled + } + } + + @objc public var overdrag: Bool = false { + didSet { + props.overdrag = overdrag + } + } + + @objc public var currentPage: Int = -1 { + didSet { + props.currentPage = currentPage + } + } + @objc public var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none { + didSet { + props.keyboardDismissMode = keyboardDismissMode + } + } + + @objc public var layoutDirection: PagerLayoutDirection = .ltr { + didSet { + props.layoutDirection = layoutDirection + } + } + @objc public var orientation: UICollectionView.ScrollDirection = .horizontal { + didSet { + props.orientation = orientation + } + } + + @objc public convenience init(delegate: PagerViewProviderDelegate) { + self.init() + self.delegate = delegate + } + + override public func didUpdateReactSubviews() { + props.children = reactSubviews().map(IdentifiablePlatformView.init) + } + + @objc(insertChild:atIndex:) + public func insertChild(_ child: UIView, at index: Int) { + guard index >= 0 && index <= props.children.count else { + return + } + props.children.insert(IdentifiablePlatformView(child), at: index) + } + + @objc(removeChildAtIndex:) + public func removeChild(at index: Int) { + guard index >= 0 && index < props.children.count else { + return + } + props.children.remove(at: index) + } + + override public func layoutSubviews() { + super.layoutSubviews() + setupView() + } + + @objc public func goTo(index: Int, animated: Bool) { + if animated { + withAnimation { + props.currentPage = index + } + } else { + props.currentPage = index + } + } + + private func setupView() { + if self.hostingController != nil { + return + } + + self.hostingController = UIHostingController(rootView: PagerView(props: props, delegate: delegate)) + if let hostingController = self.hostingController, let parentViewController = reactViewController() { + parentViewController.addChild(hostingController) + hostingController.view.backgroundColor = .clear + addSubview(hostingController.view) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.pinEdges(to: self) + } + } +} diff --git a/ios/RNCPagerViewComponentView.h b/ios/RNCPagerViewComponentView.h index 16b04b6f..e5b76dd5 100644 --- a/ios/RNCPagerViewComponentView.h +++ b/ios/RNCPagerViewComponentView.h @@ -1,5 +1,6 @@ #import #import +#ifdef __cplusplus #import NS_ASSUME_NONNULL_BEGIN @@ -9,3 +10,4 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END +#endif diff --git a/ios/RNCPagerViewComponentView.mm b/ios/RNCPagerViewComponentView.mm index cc0a1fa8..affb2495 100644 --- a/ios/RNCPagerViewComponentView.mm +++ b/ios/RNCPagerViewComponentView.mm @@ -10,25 +10,19 @@ #import "RCTOnPageScrollEvent.h" -using namespace facebook::react; - -@interface RNCPagerViewComponentView () +#if __has_include("react_native_pager_view/react_native_pager_view-Swift.h") +#import "react_native_pager_view/react_native_pager_view-Swift.h" +#else +#import "react_native_pager_view-Swift.h" +#endif -@property(nonatomic, strong) UIPageViewController *nativePageViewController; -@property(nonatomic, strong) NSMutableArray *nativeChildrenViewControllers; +using namespace facebook::react; +@interface RNCPagerViewComponentView () @end @implementation RNCPagerViewComponentView { - LayoutMetrics _layoutMetrics; - LayoutMetrics _oldLayoutMetrics; - UIScrollView *scrollView; - BOOL transitioning; - NSInteger _currentIndex; - NSInteger _destinationIndex; - BOOL _overdrag; - NSString *_layoutDirection; - BOOL _scrollEnabled; + PagerViewProvider *_pagerViewProvider; } // Needed because of this: https://github.com/facebook/react-native/pull/37274 @@ -37,383 +31,131 @@ + (void)load [super load]; } -- (void)initializeNativePageViewController { - const auto &viewProps = *std::static_pointer_cast(_props); - NSDictionary *options = @{ UIPageViewControllerOptionInterPageSpacingKey: @(viewProps.pageMargin) }; - UIPageViewControllerNavigationOrientation orientation = UIPageViewControllerNavigationOrientationHorizontal; - switch (viewProps.orientation) { - case RNCViewPagerOrientation::Horizontal: - orientation = UIPageViewControllerNavigationOrientationHorizontal; - break; - case RNCViewPagerOrientation::Vertical: - orientation = UIPageViewControllerNavigationOrientationVertical; - break; - } - _nativePageViewController = [[UIPageViewController alloc] - initWithTransitionStyle: UIPageViewControllerTransitionStyleScroll - navigationOrientation:orientation - options:options]; - _nativePageViewController.dataSource = self; - _nativePageViewController.delegate = self; - _nativePageViewController.view.frame = self.frame; - self.contentView = _nativePageViewController.view; - - for (UIView *subview in _nativePageViewController.view.subviews) { - if([subview isKindOfClass:UIScrollView.class]){ - ((UIScrollView *)subview).delegate = self; - ((UIScrollView *)subview).delaysContentTouches = NO; - scrollView = (UIScrollView *)subview; - } - } - - [self applyScrollEnabled]; -} - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); - _props = defaultProps; - _nativeChildrenViewControllers = [[NSMutableArray alloc] init]; - _currentIndex = -1; - _destinationIndex = -1; - _layoutDirection = @"ltr"; - _overdrag = NO; - _scrollEnabled = YES; - } - - return self; -} - -- (void)willMoveToSuperview:(UIView *)newSuperview { - if (newSuperview != nil) { - [self initializeNativePageViewController]; - [self goTo:_currentIndex animated:NO]; - } + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + _pagerViewProvider = [[PagerViewProvider alloc] initWithDelegate:self]; + self.contentView = _pagerViewProvider; + } + + return self; } #pragma mark - React API - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - UIViewController *vc = [UIViewController new]; - [vc.view addSubview:childComponentView]; - [_nativeChildrenViewControllers insertObject:vc atIndex:index]; - [self goTo:_currentIndex animated:NO]; + [_pagerViewProvider insertChild:childComponentView atIndex:index]; } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { - [childComponentView removeFromSuperview]; - [_nativeChildrenViewControllers removeObjectAtIndex:index]; - - NSInteger maxPage = _nativeChildrenViewControllers.count - 1; - - if (_currentIndex >= maxPage) { - [self goTo:maxPage animated:NO]; - } -} - - --(void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics { - _oldLayoutMetrics = oldLayoutMetrics; - _layoutMetrics = layoutMetrics; - - if (transitioning) { - return; - } - - [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; -} - - --(void)prepareForRecycle { - [super prepareForRecycle]; - _nativePageViewController = nil; - _currentIndex = -1; + [_pagerViewProvider removeChildAtIndex:index]; + [childComponentView removeFromSuperview]; } -- (void)shouldDismissKeyboard:(RNCViewPagerKeyboardDismissMode)dismissKeyboard { -#if !TARGET_OS_VISION - UIScrollViewKeyboardDismissMode dismissKeyboardMode = UIScrollViewKeyboardDismissModeNone; - switch (dismissKeyboard) { - case RNCViewPagerKeyboardDismissMode::None: - dismissKeyboardMode = UIScrollViewKeyboardDismissModeNone; - break; - case RNCViewPagerKeyboardDismissMode::OnDrag: - dismissKeyboardMode = UIScrollViewKeyboardDismissModeOnDrag; - break; - } - scrollView.keyboardDismissMode = dismissKeyboardMode; -#endif -} - -- (void)applyScrollEnabled { - scrollView.scrollEnabled = _scrollEnabled; ++ (BOOL)shouldBeRecycled +{ + return NO; } - - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps{ - const auto &oldScreenProps = *std::static_pointer_cast(_props); - const auto &newScreenProps = *std::static_pointer_cast(props); - - // change index only once - if (_currentIndex == -1) { - _currentIndex = newScreenProps.initialPage; - [self shouldDismissKeyboard: newScreenProps.keyboardDismissMode]; - _scrollEnabled = newScreenProps.scrollEnabled; - [self applyScrollEnabled]; - } - - const auto newLayoutDirectionStr = RCTNSStringFromString(toString(newScreenProps.layoutDirection)); - - - if (_layoutDirection != newLayoutDirectionStr) { - _layoutDirection = newLayoutDirectionStr; - } - - if (oldScreenProps.keyboardDismissMode != newScreenProps.keyboardDismissMode) { - [self shouldDismissKeyboard: newScreenProps.keyboardDismissMode]; - } - - if (oldScreenProps.scrollEnabled != newScreenProps.scrollEnabled) { - _scrollEnabled = newScreenProps.scrollEnabled; - [self applyScrollEnabled]; - } - - if (newScreenProps.overdrag != _overdrag) { - _overdrag = newScreenProps.overdrag; - } - - [super updateProps:props oldProps:oldProps]; -} - - -#pragma mark - Internal methods - -- (void)disableSwipe { - self.nativePageViewController.view.userInteractionEnabled = NO; -} - -- (void)enableSwipe { - self.nativePageViewController.view.userInteractionEnabled = YES; -} - -- (void)goTo:(NSInteger)index animated:(BOOL)animated { - NSInteger numberOfPages = _nativeChildrenViewControllers.count; - - [self disableSwipe]; - - _destinationIndex = index; - - - if (numberOfPages == 0 || index < 0 || index > numberOfPages - 1) { - return; - } - - BOOL isForward = (index > _currentIndex && [self isLtrLayout]) || (index < _currentIndex && ![self isLtrLayout]); - UIPageViewControllerNavigationDirection direction = isForward ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse; - - long diff = labs(index - _currentIndex); - - [self setPagerViewControllers:index - direction:direction - animated:diff == 0 ? NO : animated]; - -} - -- (void)setPagerViewControllers:(NSInteger)index - direction:(UIPageViewControllerNavigationDirection)direction - animated:(BOOL)animated{ - if (_nativePageViewController == nil) { - [self enableSwipe]; - return; - } + const auto &oldScreenProps = *std::static_pointer_cast(_props); + const auto &newScreenProps = *std::static_pointer_cast(props); - transitioning = YES; - - __weak RNCPagerViewComponentView *weakSelf = self; - [_nativePageViewController setViewControllers:@[[_nativeChildrenViewControllers objectAtIndex:index]] - direction:direction - animated:animated - completion:^(BOOL finished) { - self->transitioning = NO; - __strong RNCPagerViewComponentView *strongSelf = weakSelf; - [strongSelf enableSwipe]; - if (strongSelf->_eventEmitter != nullptr ) { - const auto eventEmitter = [strongSelf pagerEventEmitter]; - int position = (int) index; - eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast(position)}); - strongSelf->_currentIndex = index; - } - [strongSelf updateLayoutMetrics:strongSelf->_layoutMetrics oldLayoutMetrics:strongSelf->_oldLayoutMetrics]; - }]; -} - - -- (UIViewController *)nextControllerForController:(UIViewController *)controller - inDirection:(UIPageViewControllerNavigationDirection)direction { - NSUInteger numberOfPages = _nativeChildrenViewControllers.count; - NSInteger index = [_nativeChildrenViewControllers indexOfObject:controller]; - - if (index == NSNotFound) { - return nil; - } - - direction == UIPageViewControllerNavigationDirectionForward ? index++ : index--; - - if (index < 0 || (index > (numberOfPages - 1))) { - return nil; - } - - return [_nativeChildrenViewControllers objectAtIndex:index]; -} - -- (UIViewController *)currentlyDisplayed { - return _nativePageViewController.viewControllers.firstObject; -} - -#pragma mark - UIScrollViewDelegate + if (_pagerViewProvider.currentPage == -1) { + _pagerViewProvider.currentPage = newScreenProps.initialPage; + } -- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { - const auto eventEmitter = [self pagerEventEmitter]; - eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging }); -} - -- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { - - const auto eventEmitter = [self pagerEventEmitter]; - eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Settling }); - - if (!_overdrag) { - NSInteger maxIndex = _nativeChildrenViewControllers.count - 1; - BOOL isFirstPage = [self isLtrLayout] ? _currentIndex == 0 : _currentIndex == maxIndex; - BOOL isLastPage = [self isLtrLayout] ? _currentIndex == maxIndex : _currentIndex == 0; - CGFloat contentOffset = [self isHorizontal] ? scrollView.contentOffset.x : scrollView.contentOffset.y; - CGFloat topBound = [self isHorizontal] ? scrollView.bounds.size.width : scrollView.bounds.size.height; + if (oldScreenProps.scrollEnabled != newScreenProps.scrollEnabled) { + _pagerViewProvider.scrollEnabled = newScreenProps.scrollEnabled; + } + + if (oldScreenProps.overdrag != newScreenProps.overdrag) { + _pagerViewProvider.overdrag = newScreenProps.overdrag; + } + + if (oldScreenProps.keyboardDismissMode != newScreenProps.keyboardDismissMode) { + switch (newScreenProps.keyboardDismissMode) { + case RNCViewPagerKeyboardDismissMode::None: + _pagerViewProvider.keyboardDismissMode = UIScrollViewKeyboardDismissModeNone; + break; - if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) { - CGPoint croppedOffset = [self isHorizontal] ? CGPointMake(topBound, 0) : CGPointMake(0, topBound); - *targetContentOffset = croppedOffset; - - eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle }); - } + case RNCViewPagerKeyboardDismissMode::OnDrag: + _pagerViewProvider.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; } -} + } + + if (oldScreenProps.orientation != newScreenProps.orientation) { + _pagerViewProvider.orientation = newScreenProps.orientation == RNCViewPagerOrientation::Vertical ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal; + } + + if (oldScreenProps.layoutDirection != newScreenProps.layoutDirection) { + _pagerViewProvider.layoutDirection = newScreenProps.layoutDirection == RNCViewPagerLayoutDirection::Rtl ? PagerLayoutDirectionRtl : PagerLayoutDirectionLtr; + } -- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { - const auto eventEmitter = [self pagerEventEmitter]; - eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle }); + [super updateProps:props oldProps:oldProps]; } +#pragma mark - PagerViewProviderDelegate -- (void)scrollViewDidScroll:(UIScrollView *)scrollView { - BOOL isHorizontal = [self isHorizontal]; - CGFloat contentOffset = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y; - CGFloat frameSize = isHorizontal ? scrollView.frame.size.width : scrollView.frame.size.height; - - if (frameSize == 0) { - return; - } - - float offset = (contentOffset - frameSize) / frameSize; - float absoluteOffset = fabs(offset); - NSInteger position = _currentIndex; - - BOOL isHorizontalRtl = [self isHorizontalRtlLayout]; - BOOL isAnimatingBackwards = isHorizontalRtl ? offset > 0.05f : offset < 0; - BOOL isBeingMovedByNestedScrollView = !scrollView.isDragging && !scrollView.isTracking; - if (scrollView.isDragging || isBeingMovedByNestedScrollView) { - _destinationIndex = isAnimatingBackwards ? _currentIndex - 1 : _currentIndex + 1; - } - - if (isAnimatingBackwards) { - position = _destinationIndex; - absoluteOffset = fmax(0, 1 - absoluteOffset); - } - - if (!_overdrag) { - NSInteger maxIndex = _nativeChildrenViewControllers.count - 1; - NSInteger firstPageIndex = isHorizontalRtl ? maxIndex : 0; - NSInteger lastPageIndex = isHorizontalRtl ? 0 : maxIndex; - BOOL isFirstPage = _currentIndex == firstPageIndex; - BOOL isLastPage = _currentIndex == lastPageIndex; - CGFloat topBound = isHorizontal ? scrollView.bounds.size.width : scrollView.bounds.size.height; - - if ((isFirstPage && contentOffset <= topBound) || (isLastPage && contentOffset >= topBound)) { - CGPoint croppedOffset = isHorizontal ? CGPointMake(topBound, 0) : CGPointMake(0, topBound); - scrollView.contentOffset = croppedOffset; - absoluteOffset = 0; - position = isLastPage ? lastPageIndex : firstPageIndex; - } - } - - float interpolatedOffset = absoluteOffset * labs(_destinationIndex - _currentIndex); - [self sendScrollEventsForPosition:position offset:interpolatedOffset]; +- (void)onPageScrollWithData:(OnPageScrollEventData *)data { + const auto eventEmitter = [self pagerEventEmitter]; + [self sendScrollEventsForPosition:data.position offset:data.offset]; } - -#pragma mark - UIPageViewControllerDelegate - -- (void)pageViewController:(UIPageViewController *)pageViewController - didFinishAnimating:(BOOL)finished - previousViewControllers:(nonnull NSArray *)previousViewControllers - transitionCompleted:(BOOL)completed { - if (completed) { - UIViewController* currentVC = [self currentlyDisplayed]; - NSUInteger currentIndex = [_nativeChildrenViewControllers indexOfObject:currentVC]; - _currentIndex = currentIndex; - int position = (int) currentIndex; - const auto eventEmitter = [self pagerEventEmitter]; - eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast(position)}); - } +- (void)onPageSelectedWithPosition:(NSInteger)position { + const auto eventEmitter = [self pagerEventEmitter]; + eventEmitter->onPageSelected(RNCViewPagerEventEmitter::OnPageSelected{.position = static_cast(position)}); } -#pragma mark - UIPageViewControllerDataSource - -- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController - viewControllerAfterViewController:(UIViewController *)viewController { - - UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionForward : UIPageViewControllerNavigationDirectionReverse; - return [self nextControllerForController:viewController inDirection:direction]; +- (void)onPageScrollStateChangedWithState:(enum PageScrollState)state { + const auto eventEmitter = [self pagerEventEmitter]; + + RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState scrollState; + + switch (state) { + case PageScrollStateIdle: + scrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle; + break; + + case PageScrollStateDragging: + scrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging; + break; + + case PageScrollStateSettling: + scrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Settling; + break; + } + + eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{ + .pageScrollState = scrollState + }); } -- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController - viewControllerBeforeViewController:(UIViewController *)viewController { - UIPageViewControllerNavigationDirection direction = [self isLtrLayout] ? UIPageViewControllerNavigationDirectionReverse : UIPageViewControllerNavigationDirectionForward; - return [self nextControllerForController:viewController inDirection:direction]; +#pragma mark - Internal methods + +- (void)goTo:(NSInteger)index animated:(BOOL)animated { + [_pagerViewProvider goToIndex:index animated:animated]; } #pragma mark - Imperative methods exposed to React Native - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { - RCTRNCViewPagerHandleCommand(self, commandName, args); + RCTRNCViewPagerHandleCommand(self, commandName, args); } - (void)setPage:(NSInteger)index { - [self goTo:index animated:YES]; + [self goTo:index animated:YES]; } - (void)setPageWithoutAnimation:(NSInteger)index { - [self goTo:index animated:NO]; + [self goTo:index animated:NO]; } - (void)setScrollEnabledImperatively:(BOOL)scrollEnabled { - _scrollEnabled = scrollEnabled; - [self applyScrollEnabled]; -} - -#pragma mark - Helpers - -- (BOOL)isHorizontalRtlLayout { - return self.isHorizontal && !self.isLtrLayout; -} - -- (BOOL)isHorizontal { - return _nativePageViewController.navigationOrientation == UIPageViewControllerNavigationOrientationHorizontal; -} - -- (BOOL)isLtrLayout { - return [_layoutDirection isEqualToString: @"ltr"]; } - (std::shared_ptr)pagerEventEmitter @@ -421,40 +163,40 @@ - (BOOL)isLtrLayout { if (!_eventEmitter) { return nullptr; } - + assert(std::dynamic_pointer_cast(_eventEmitter)); return std::static_pointer_cast(_eventEmitter); } - (void)sendScrollEventsForPosition:(NSInteger)position offset:(CGFloat)offset { - const auto eventEmitter = [self pagerEventEmitter]; - eventEmitter->onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{ - .position = static_cast(position), - .offset = offset - }); + const auto eventEmitter = [self pagerEventEmitter]; + eventEmitter->onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{ + .position = static_cast(position), + .offset = offset + }); - // This is temporary workaround to allow animations based on onPageScroll event - // until Fabric implements proper NativeAnimationDriver, - // see: https://github.com/facebook/react-native/blob/44f431b471c243c92284aa042d3807ba4d04af65/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L59 - RCTOnPageScrollEvent *event = [[RCTOnPageScrollEvent alloc] initWithReactTag:@(self.tag) - position:@(position) - offset:@(offset)]; - NSDictionary *userInfo = @{@"event": event}; - [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED" - object:nil - userInfo:userInfo]; + // This is temporary workaround to allow animations based on onPageScroll event + // until Fabric implements proper NativeAnimationDriver, + // see: https://github.com/facebook/react-native/blob/44f431b471c243c92284aa042d3807ba4d04af65/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L59 + RCTOnPageScrollEvent *event = [[RCTOnPageScrollEvent alloc] initWithReactTag:@(self.tag) + position:@(position) + offset:@(offset)]; + NSDictionary *userInfo = @{@"event": event}; + [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED" + object:nil + userInfo:userInfo]; } #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider { - return concreteComponentDescriptorProvider(); + return concreteComponentDescriptorProvider(); } @end Class RNCViewPagerCls(void) { - return RNCPagerViewComponentView.class; + return RNCPagerViewComponentView.class; } diff --git a/react-native-pager-view.podspec b/react-native-pager-view.podspec index 2c2f8a8b..0ea42a10 100644 --- a/react-native-pager-view.podspec +++ b/react-native-pager-view.podspec @@ -13,7 +13,9 @@ Pod::Spec.new do |s| s.platforms = { :ios => "10.0", :visionos => "1.0" } s.source = { :git => "https://github.com/callstack/react-native-pager-view.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}" + s.source_files = "ios/**/*.{h,m,mm,swift}" + + s.dependency "SwiftUIIntrospect", '~> 1.0' install_modules_dependencies(s) diff --git a/src/PagerView.tsx b/src/PagerView.tsx index 5238f1d1..26b33c20 100644 --- a/src/PagerView.tsx +++ b/src/PagerView.tsx @@ -3,9 +3,7 @@ import { Platform, Keyboard } from 'react-native'; import { I18nManager } from 'react-native'; import type * as ReactNative from 'react-native'; -import { - childrenWithOverriddenStyle, -} from './utils'; +import { childrenWithOverriddenStyle } from './utils'; import PagerViewNativeComponent, { Commands as PagerViewNativeCommands, @@ -15,7 +13,6 @@ import PagerViewNativeComponent, { NativeProps, } from './PagerViewNativeComponent'; - /** * Container that allows to flip left and right between child views. Each * child view of the `PagerView` will be treated as a separate page @@ -62,7 +59,6 @@ export class PagerView extends React.Component { private isScrolling = false; pagerView: React.ElementRef | null = null; - private get deducedLayoutDirection() { if ( !this.props.layoutDirection || @@ -149,22 +145,20 @@ export class PagerView extends React.Component { }; render() { - return ( - { - this.pagerView = ref; - }} - style={this.props.style} - layoutDirection={this.deducedLayoutDirection} - onPageScroll={this._onPageScroll} - onPageScrollStateChanged={this._onPageScrollStateChanged} - onPageSelected={this._onPageSelected} - onMoveShouldSetResponderCapture={ - this._onMoveShouldSetResponderCapture - } - children={childrenWithOverriddenStyle(this.props.children)} - /> - ); + return ( + { + this.pagerView = ref; + }} + style={this.props.style} + layoutDirection={this.deducedLayoutDirection} + onPageScroll={this._onPageScroll} + onPageScrollStateChanged={this._onPageScrollStateChanged} + onPageSelected={this._onPageSelected} + onMoveShouldSetResponderCapture={this._onMoveShouldSetResponderCapture} + children={childrenWithOverriddenStyle(this.props.children)} + /> + ); } } From 766e57861d49c8dd5f3cec4ad94f953d9eaaffd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Tue, 9 Dec 2025 16:57:06 +0100 Subject: [PATCH 2/5] feat: improve safe area handling --- ios/Extensions.swift | 35 +++++++++++++++++++++++++++++++++++ ios/PagerViewProvider.swift | 18 ++++++++---------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/ios/Extensions.swift b/ios/Extensions.swift index 0fb0d1ed..22641a82 100644 --- a/ios/Extensions.swift +++ b/ios/Extensions.swift @@ -37,3 +37,38 @@ extension UIView { } } +extension UIHostingController { + convenience public init(rootView: Content, ignoreSafeArea: Bool) { + self.init(rootView: rootView) + + if ignoreSafeArea { + disableSafeArea() + } + } + + /// Disables safe area insets by dynamically subclassing the hosting controller's view + /// and overriding safeAreaInsets to return .zero. + func disableSafeArea() { + guard let viewClass = object_getClass(view) else { return } + + let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea") + if let viewSubclass = NSClassFromString(viewSubclassName) { + object_setClass(view, viewSubclass) + } + else { + guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } + guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } + + if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { + let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in + return .zero + } + class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) + } + + objc_registerClassPair(viewSubclass) + object_setClass(view, viewSubclass) + } + } +} + diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift index 50b85f32..db3c6db4 100644 --- a/ios/PagerViewProvider.swift +++ b/ios/PagerViewProvider.swift @@ -1,8 +1,6 @@ import SwiftUI import UIKit - - @objc public enum PageScrollState: Int { case idle case dragging @@ -70,10 +68,6 @@ import UIKit self.delegate = delegate } - override public func didUpdateReactSubviews() { - props.children = reactSubviews().map(IdentifiablePlatformView.init) - } - @objc(insertChild:atIndex:) public func insertChild(_ child: UIView, at index: Int) { guard index >= 0 && index <= props.children.count else { @@ -110,13 +104,17 @@ import UIKit return } - self.hostingController = UIHostingController(rootView: PagerView(props: props, delegate: delegate)) - if let hostingController = self.hostingController, let parentViewController = reactViewController() { + self.hostingController = UIHostingController( + rootView: PagerView(props: props, delegate: delegate), + ignoreSafeArea: true + ) + if let hostingController, let parentViewController = reactViewController() { parentViewController.addChild(hostingController) hostingController.view.backgroundColor = .clear addSubview(hostingController.view) - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - hostingController.view.pinEdges(to: self) + hostingController.view.frame = bounds + + hostingController.didMove(toParent: parentViewController) } } } From 6ad35ce1d319c4c134149102546dc7a77a40e699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 10 Dec 2025 09:40:22 +0100 Subject: [PATCH 3/5] feat: improve scroll delegate --- ios/PagerScrollDelegate.swift | 44 ++++++++++---------------------- ios/PagerViewProvider.swift | 4 ++- ios/RNCPagerViewComponentView.mm | 29 +++++++++++---------- 3 files changed, 31 insertions(+), 46 deletions(-) diff --git a/ios/PagerScrollDelegate.swift b/ios/PagerScrollDelegate.swift index 183a8c80..962ee7b8 100644 --- a/ios/PagerScrollDelegate.swift +++ b/ios/PagerScrollDelegate.swift @@ -4,11 +4,21 @@ 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 + private let handledSelectors: Set = [ + #selector(scrollViewDidScroll(_:)), + #selector(scrollViewWillBeginDragging(_:)), + #selector(scrollViewWillBeginDecelerating(_:)), + #selector(scrollViewDidEndDecelerating(_:)), + #selector(scrollViewDidEndScrollingAnimation(_:)), + #selector(scrollViewDidEndDragging(_:willDecelerate:)), + #selector(collectionView(_:didEndDisplaying:forItemAt:)), + #selector(collectionView(_:willDisplay:forItemAt:)) + ] + func scrollViewDidScroll(_ scrollView: UIScrollView) { let isHorizontal = orientation == .horizontal let pageSize = isHorizontal ? scrollView.frame.width : scrollView.frame.height @@ -60,39 +70,11 @@ class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDeleg } 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 + handledSelectors.contains(aSelector) || (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 + handledSelectors.contains(aSelector) ? nil : originalDelegate } } diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift index db3c6db4..c5429882 100644 --- a/ios/PagerViewProvider.swift +++ b/ios/PagerViewProvider.swift @@ -112,7 +112,9 @@ import UIKit parentViewController.addChild(hostingController) hostingController.view.backgroundColor = .clear addSubview(hostingController.view) - hostingController.view.frame = bounds + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.pinEdges(to: self) hostingController.didMove(toParent: parentViewController) } diff --git a/ios/RNCPagerViewComponentView.mm b/ios/RNCPagerViewComponentView.mm index affb2495..eeeecde6 100644 --- a/ios/RNCPagerViewComponentView.mm +++ b/ios/RNCPagerViewComponentView.mm @@ -40,7 +40,7 @@ - (instancetype)initWithFrame:(CGRect)frame _pagerViewProvider = [[PagerViewProvider alloc] initWithDelegate:self]; self.contentView = _pagerViewProvider; } - + return self; } @@ -64,7 +64,7 @@ + (BOOL)shouldBeRecycled - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps{ const auto &oldScreenProps = *std::static_pointer_cast(_props); const auto &newScreenProps = *std::static_pointer_cast(props); - + if (_pagerViewProvider.currentPage == -1) { _pagerViewProvider.currentPage = newScreenProps.initialPage; } @@ -72,26 +72,26 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(cons if (oldScreenProps.scrollEnabled != newScreenProps.scrollEnabled) { _pagerViewProvider.scrollEnabled = newScreenProps.scrollEnabled; } - + if (oldScreenProps.overdrag != newScreenProps.overdrag) { _pagerViewProvider.overdrag = newScreenProps.overdrag; } - + if (oldScreenProps.keyboardDismissMode != newScreenProps.keyboardDismissMode) { switch (newScreenProps.keyboardDismissMode) { case RNCViewPagerKeyboardDismissMode::None: _pagerViewProvider.keyboardDismissMode = UIScrollViewKeyboardDismissModeNone; break; - + case RNCViewPagerKeyboardDismissMode::OnDrag: _pagerViewProvider.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; } } - + if (oldScreenProps.orientation != newScreenProps.orientation) { _pagerViewProvider.orientation = newScreenProps.orientation == RNCViewPagerOrientation::Vertical ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal; } - + if (oldScreenProps.layoutDirection != newScreenProps.layoutDirection) { _pagerViewProvider.layoutDirection = newScreenProps.layoutDirection == RNCViewPagerLayoutDirection::Rtl ? PagerLayoutDirectionRtl : PagerLayoutDirectionLtr; } @@ -113,23 +113,23 @@ - (void)onPageSelectedWithPosition:(NSInteger)position { - (void)onPageScrollStateChangedWithState:(enum PageScrollState)state { const auto eventEmitter = [self pagerEventEmitter]; - + RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState scrollState; - + switch (state) { case PageScrollStateIdle: scrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Idle; break; - + case PageScrollStateDragging: scrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging; break; - + case PageScrollStateSettling: scrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Settling; break; } - + eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{ .pageScrollState = scrollState }); @@ -156,6 +156,7 @@ - (void)setPageWithoutAnimation:(NSInteger)index { } - (void)setScrollEnabledImperatively:(BOOL)scrollEnabled { + _pagerViewProvider.scrollEnabled = scrollEnabled; } - (std::shared_ptr)pagerEventEmitter @@ -163,7 +164,7 @@ - (void)setScrollEnabledImperatively:(BOOL)scrollEnabled { if (!_eventEmitter) { return nullptr; } - + assert(std::dynamic_pointer_cast(_eventEmitter)); return std::static_pointer_cast(_eventEmitter); } @@ -174,7 +175,7 @@ - (void)sendScrollEventsForPosition:(NSInteger)position offset:(CGFloat)offset { .position = static_cast(position), .offset = offset }); - + // This is temporary workaround to allow animations based on onPageScroll event // until Fabric implements proper NativeAnimationDriver, // see: https://github.com/facebook/react-native/blob/44f431b471c243c92284aa042d3807ba4d04af65/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm#L59 From f217dc7edcfb986ae958935231a3afdd578e1856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 10 Dec 2025 09:49:56 +0100 Subject: [PATCH 4/5] feat: move to didMoveToWindow --- ios/PagerViewProvider.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift index c5429882..54fb5177 100644 --- a/ios/PagerViewProvider.swift +++ b/ios/PagerViewProvider.swift @@ -84,9 +84,11 @@ import UIKit props.children.remove(at: index) } - override public func layoutSubviews() { - super.layoutSubviews() - setupView() + override public func didMoveToWindow() { + super.didMoveToWindow() + if window != nil { + setupView() + } } @objc public func goTo(index: Int, animated: Bool) { From 33fc6b1f43007490c4e66e97f148683329a98f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Wed, 10 Dec 2025 12:39:25 +0100 Subject: [PATCH 5/5] feat: page margin --- src/PagerView.tsx | 14 ++++++++++++-- src/utils.ios.tsx | 25 +++++++++++++++++++++++++ src/utils.tsx | 7 ++++--- 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/utils.ios.tsx diff --git a/src/PagerView.tsx b/src/PagerView.tsx index 26b33c20..5bb6401c 100644 --- a/src/PagerView.tsx +++ b/src/PagerView.tsx @@ -151,13 +151,23 @@ export class PagerView extends React.Component { ref={(ref) => { this.pagerView = ref; }} - style={this.props.style} + style={[ + this.props.style, + Platform.OS === 'ios' && this.props.pageMargin + ? { + marginHorizontal: -this.props.pageMargin / 2, + } + : null, + ]} layoutDirection={this.deducedLayoutDirection} onPageScroll={this._onPageScroll} onPageScrollStateChanged={this._onPageScrollStateChanged} onPageSelected={this._onPageSelected} onMoveShouldSetResponderCapture={this._onMoveShouldSetResponderCapture} - children={childrenWithOverriddenStyle(this.props.children)} + children={childrenWithOverriddenStyle( + this.props.children, + this.props.pageMargin + )} /> ); } diff --git a/src/utils.ios.tsx b/src/utils.ios.tsx new file mode 100644 index 00000000..7bb5a339 --- /dev/null +++ b/src/utils.ios.tsx @@ -0,0 +1,25 @@ +import React, { Children, ReactNode } from 'react'; +import { StyleSheet, View } from 'react-native'; + +export const childrenWithOverriddenStyle = ( + children?: ReactNode, + pageMargin = 0 +) => { + return Children.map(children, (child) => { + const element = child as React.ReactElement; + return ( + + {React.cloneElement(element, { + ...element.props, + style: [element.props.style, StyleSheet.absoluteFill], + })} + + ); + }); +}; diff --git a/src/utils.tsx b/src/utils.tsx index b2b9c8a1..bf2367a5 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,15 +1,16 @@ import React, { Children, ReactNode } from 'react'; import { StyleSheet, View } from 'react-native'; -export const childrenWithOverriddenStyle = (children?: ReactNode) => { +export const childrenWithOverriddenStyle = ( + children?: ReactNode, + _pageMargin = 0 +) => { return Children.map(children, (child) => { const element = child as React.ReactElement; return ( - // Add a wrapper to ensure layout is calculated correctly {React.cloneElement(element, { ...element.props, - // Override styles so that each page will fill the parent. style: [element.props.style, StyleSheet.absoluteFill], })}