Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ jobs:
pull-requests: write
uses: ./.github/workflows/ios.yml

ios-spm:
name: iOS Build (SwiftPM)
needs: changes
if: ${{ !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.ios == 'true') }}
permissions:
contents: read
pull-requests: write
uses: ./.github/workflows/ios.yml
with:
spm: true

required:
name: Required checks
needs: [changes, lint, android, ios]
Comment thread
krozniata marked this conversation as resolved.
Expand Down
26 changes: 23 additions & 3 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: iOS Build

on:
workflow_call:
inputs:
spm:
description: Convert the example to Swift Package Manager before building
type: boolean
default: false

jobs:
ios-build:
Expand Down Expand Up @@ -32,6 +37,7 @@ jobs:
# Exact key only: a Pods dir restored from an older Podfile.lock keeps stale
# `Local Podspecs` and makes `pod install` bail on the version mismatch.
- name: Cache Pods
if: ${{ !inputs.spm }}
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: example/ios/Pods
Expand All @@ -44,13 +50,27 @@ jobs:
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .ccache
key: ccache-ios-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: ccache-ios-
key: ccache-ios${{ inputs.spm && '-spm' || '' }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: ccache-ios${{ inputs.spm && '-spm' || '' }}-

- name: Install example Pods
if: ${{ !inputs.spm }}
run: bun pods
working-directory: example

- name: Convert example to SwiftPM
if: ${{ inputs.spm }}
working-directory: example/ios
run: |
npx react-native spm scaffold --deintegrate --yes

# The scaffolder misses screens' `<rnscreens/...>` header path.
manifest=../../node_modules/react-native-screens/Package.swift
search_path='.headerSearchPath("common/cpp/react/renderer/components")'
grep -qF "$search_path" "$manifest" ||
perl -pi -e 's|\.headerSearchPath\("common/cpp"\),|$&'" $search_path,"'|g' "$manifest"
grep -qF "$search_path" "$manifest"

# Simulator.app attaches the Mac keyboard by default, which hides the
# software keyboard and leaves no Return key for the keyboard tests.
- name: Detach hardware keyboard from simulator
Expand All @@ -74,7 +94,7 @@ jobs:
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-report-ios
name: e2e-report-ios${{ inputs.spm && '-spm' || '' }}
path: |
e2e/.e2e/report.json
e2e/.e2e/artifacts
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@ example/ios/PagerViewExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
!.yarn/sdks
!.yarn/versions
.agent-device

# Swift Package Manager
.build/
.swiftpm/
ios/Package.resolved
77 changes: 77 additions & 0 deletions ios/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// swift-tools-version: 6.0

// Swift Package Manager manifest consumed by React Native's SPM autolinking
// (`npx react-native spm`). CocoaPods keeps using react-native-pager-view.podspec.
//
// The autolinker references this package through a symlink at
// <app>/ios/build/generated/autolinking/libs/react-native-pager-view, and SwiftPM
// resolves the relative `.package(path:)` entries below against that symlink,
// so they point at the app's prebuilt React Native and codegen packages.

import PackageDescription

let reactNativeHeaders: [Target.Dependency] = [
.product(name: "ReactHeaders", package: "ReactNative"),
.product(name: "ReactNativeHeaders", package: "ReactNative"),
.product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"),
.product(name: "ReactAppHeaders", package: "React-GeneratedCode"),
]

// Must match the prebuilt React.framework's config-gated C++ ABI, otherwise the
// Release link fails on DebugStringConvertible's vtable.
let reactCxxDefines: [CXXSetting] = [
.define("DEBUG", .when(configuration: .debug)),
.define("NDEBUG", .when(configuration: .release)),
]

let package = Package(
name: "react-native-pager-view",
platforms: [.iOS("15.1"), .visionOS(.v1)],
products: [
.library(name: "react-native-pager-view", targets: ["react-native-pager-view", "RNCPagerViewSwift"]),
],
dependencies: [
.package(name: "ReactNative", path: "../../../../xcframeworks"),
.package(name: "React-GeneratedCode", path: "../../../ios"),
.package(url: "https://github.com/siteline/swiftui-introspect", from: "1.0.0"),
],
targets: [
// SwiftPM can't mix Swift and ObjC++ in one target, so the Swift
// sources build separately. ObjC++ can't import Swift (no C++ modules),
// so the dependency runs the other way: Swift implements the ObjC API
// in RNCPagerViewProviding.h, and both targets ship in the product.
.target(
name: "RNCPagerViewSwift",
dependencies: [
"react-native-pager-view",
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
] + reactNativeHeaders,
path: ".",
sources: [
"Extensions.swift",
"PagerScrollDelegate.swift",
"PagerView.swift",
"PagerViewIOS15Compat.swift",
"PagerViewProps.swift",
"PagerViewProvider.swift",
],
swiftSettings: [.swiftLanguageMode(.v5)]
),
.target(
name: "react-native-pager-view",
dependencies: reactNativeHeaders,
path: ".",
sources: [
"RCTOnPageScrollEvent.m",
"RNCPagerViewComponentView.mm",
],
publicHeadersPath: ".",
cxxSettings: reactCxxDefines,
linkerSettings: [
.linkedFramework("UIKit"),
.linkedFramework("SwiftUI"),
]
),
],
cxxLanguageStandard: .cxx20
)
9 changes: 5 additions & 4 deletions ios/PagerScrollDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import UIKit
#if SWIFT_PACKAGE
import react_native_pager_view
#endif

/**
Scroll delegate used to control underlying TabView's collection view.
Expand Down Expand Up @@ -29,8 +32,7 @@ class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDeleg
let offset = contentOffset.truncatingRemainder(dividingBy: pageSize) / pageSize
let position = round(contentOffset / pageSize - offset)

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

Expand All @@ -43,8 +45,7 @@ class PagerScrollDelegate: NSObject, UIScrollViewDelegate, UICollectionViewDeleg

if pageSize > 0 {
let page = Int(round(contentOffset / pageSize))
let eventData = OnPageScrollEventData(position: Double(page), offset: 0)
delegate?.onPageScroll(data: eventData)
delegate?.onPageScroll(position: Double(page), offset: 0)
}

delegate?.onPageScrollStateChanged(state: .idle)
Expand Down
3 changes: 3 additions & 0 deletions ios/PagerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
#if SWIFT_PACKAGE
import react_native_pager_view
#endif

struct PagerView: View {
@ObservedObject var props: PagerViewProps
Expand Down
8 changes: 4 additions & 4 deletions ios/PagerViewProps.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import SwiftUI
import UIKit
#if SWIFT_PACKAGE
import react_native_pager_view
#endif

struct IdentifiablePlatformView: Identifiable, Equatable {
let id = UUID()
Expand All @@ -10,10 +13,7 @@ struct IdentifiablePlatformView: Identifiable, Equatable {
}
}

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

extension PagerLayoutDirection {
var converted: LayoutDirection {
switch self {
case .ltr:
Expand Down
68 changes: 27 additions & 41 deletions ios/PagerViewProvider.swift
Original file line number Diff line number Diff line change
@@ -1,131 +1,117 @@
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)
#if SWIFT_PACKAGE
// CocoaPods exposes React's UIView+React category and RNCPagerViewProviding.h
// implicitly (same module); SwiftPM needs the imports.
import React
import react_native_pager_view
#endif

@_cdecl("RNCPagerViewCreateProvider")
public func createPagerViewProvider(delegate: PagerViewProviderDelegate) -> UIView & RNCPagerViewProviding {
Comment thread
krozniata marked this conversation as resolved.
PagerViewProvider(delegate: delegate)
}

@objc public class PagerViewProvider: UIView {
final class PagerViewProvider: UIView, RNCPagerViewProviding {
private weak var delegate: PagerViewProviderDelegate?
private var hostingController: UIHostingController<PagerView>?
private var props = PagerViewProps()

@objc public var scrollEnabled: Bool = true {
var scrollEnabled: Bool = true {
didSet {
props.scrollEnabled = scrollEnabled
}
}

@objc public var overdrag: Bool = false {
var overdrag: Bool = false {
didSet {
props.overdrag = overdrag
}
}

@objc public var currentPage: Int = -1 {
var currentPage: Int = -1 {
didSet {
props.currentPage = currentPage
}
}
@objc public var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none {
var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none {
didSet {
props.keyboardDismissMode = keyboardDismissMode
}
}

@objc public var layoutDirection: PagerLayoutDirection = .ltr {
var layoutDirection: PagerLayoutDirection = .ltr {
didSet {
props.layoutDirection = layoutDirection
}
}
@objc public var orientation: UICollectionView.ScrollDirection = .horizontal {
var orientation: UICollectionView.ScrollDirection = .horizontal {
didSet {
props.orientation = orientation
}
}

@objc public convenience init(delegate: PagerViewProviderDelegate) {
convenience init(delegate: PagerViewProviderDelegate) {
self.init()
self.delegate = delegate
}

@objc(insertChild:atIndex:)
public func insertChild(_ child: UIView, at index: Int) {
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) {
func removeChild(at index: Int) {
guard index >= 0 && index < props.children.count else {
return
}
props.children.remove(at: index)
}

override public func didMoveToWindow() {
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
setupView()
}
}

override public func willMove(toWindow newWindow: UIWindow?) {
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if newWindow != nil {
syncParentViewController()
}
}

override public func willMove(toSuperview newSuperview: UIView?) {
override func willMove(toSuperview newSuperview: UIView?) {
if let parentViewController = newSuperview?.reactViewController() {
syncParentViewController(to: parentViewController)
}
super.willMove(toSuperview: newSuperview)
}

override public func didMoveToSuperview() {
override func didMoveToSuperview() {
super.didMoveToSuperview()
syncParentViewController()
}

override public func layoutSubviews() {
override func layoutSubviews() {
super.layoutSubviews()
if window != nil {
setupView()
}
}

override public func safeAreaInsetsDidChange() {
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
// The hosting view is forced to `.zero`, so UIKit will not forward this
// change into `PageChildViewController`. Relayout so it can re-read our
// insets (native search bar, keyboard, stacked header, …).
hostingController?.view.setNeedsLayout()
}

@objc public func goTo(index: Int, animated: Bool) {
func goTo(index: Int, animated: Bool) {
if animated && hasPresentedViewController() {
// A native-stack modal can begin its dismissal in the same JavaScript
// callback as `setPage`. Starting a SwiftUI TabView animation in that
Expand Down
Loading
Loading