diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 27891d05..daa88b08 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 (6.8.1): + - react-native-pager-view (7.0.0): - 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: 919534782a0489f7e2aeeb9a8b8959edfd3f067a + react-native-pager-view: 3bdf418f13ca0eb979c2720b8991a5f46f59386e 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 ccc2595b..affb2495 100644 --- a/ios/RNCPagerViewComponentView.mm +++ b/ios/RNCPagerViewComponentView.mm @@ -10,24 +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; + PagerViewProvider *_pagerViewProvider; } // Needed because of this: https://github.com/facebook/react-native/pull/37274 @@ -36,372 +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; - } - } -} - (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; - } - - 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 ++ (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]; - } - - const auto newLayoutDirectionStr = RCTNSStringFromString(toString(newScreenProps.layoutDirection)); - - - if (_layoutDirection != newLayoutDirectionStr) { - _layoutDirection = newLayoutDirectionStr; - } - - if (oldScreenProps.keyboardDismissMode != newScreenProps.keyboardDismissMode) { - [self shouldDismissKeyboard: newScreenProps.keyboardDismissMode]; - } - - if (newScreenProps.scrollEnabled != scrollView.scrollEnabled) { - scrollView.scrollEnabled = newScreenProps.scrollEnabled; - } - - 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 - -- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { - const auto eventEmitter = [self pagerEventEmitter]; - eventEmitter->onPageScrollStateChanged(RNCViewPagerEventEmitter::OnPageScrollStateChanged{.pageScrollState = RNCViewPagerEventEmitter::OnPageScrollStateChangedPageScrollState::Dragging }); -} + if (_pagerViewProvider.currentPage == -1) { + _pagerViewProvider.currentPage = newScreenProps.initialPage; + } -- (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 { - [scrollView setScrollEnabled:scrollEnabled]; -} - -#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 @@ -409,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)} + /> + ); } } diff --git a/src/utils.tsx b/src/utils.tsx index b2b9c8a1..2060301c 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -10,7 +10,7 @@ export const childrenWithOverriddenStyle = (children?: ReactNode) => { {React.cloneElement(element, { ...element.props, // Override styles so that each page will fill the parent. - style: [element.props.style, StyleSheet.absoluteFill], + style: [element.props.style, { width: '100%', height: '100%' }], })} );