react-native-pdfrender 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +306 -0
  2. package/android/build.gradle +76 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/com/pdfrender/ComposeRenderer.kt +85 -0
  5. package/android/src/main/java/com/pdfrender/PdfCacheManager.kt +150 -0
  6. package/android/src/main/java/com/pdfrender/PdfConstants.kt +63 -0
  7. package/android/src/main/java/com/pdfrender/PdfIconComponents.kt +275 -0
  8. package/android/src/main/java/com/pdfrender/PdfRenderingLogic.kt +325 -0
  9. package/android/src/main/java/com/pdfrender/PdfUIComponents.kt +335 -0
  10. package/android/src/main/java/com/pdfrender/PdfViewPackage.kt +32 -0
  11. package/android/src/main/java/com/pdfrender/PdfViewerActivity.kt +3467 -0
  12. package/android/src/main/java/com/pdfrender/PdfViewerFabricManager.kt +244 -0
  13. package/android/src/main/java/com/pdfrender/PdfViewerFragment.kt +129 -0
  14. package/android/src/main/java/com/pdfrender/PdfViewerTurboModule.kt +158 -0
  15. package/android/src/main/java/com/pdfrender/events/FullScreenChangeEvent.kt +26 -0
  16. package/android/src/main/java/com/pdfrender/events/LeftScreenChangeEvent.kt +22 -0
  17. package/android/src/main/java/com/pdfrender/events/RightScreenChangeEvent.kt +22 -0
  18. package/android/src/main/java/com/pdfrender/events/ZoomChangeEvent.kt +22 -0
  19. package/ios/PdfCacheManager.swift +44 -0
  20. package/ios/PdfConstants.swift +38 -0
  21. package/ios/PdfPageView.swift +121 -0
  22. package/ios/PdfRenderingLogic.swift +107 -0
  23. package/ios/PdfToolbarView.swift +158 -0
  24. package/ios/PdfViewerComponentView.mm +194 -0
  25. package/ios/PdfViewerTurboModule.mm +186 -0
  26. package/ios/PdfViewerTurboModuleImpl.swift +141 -0
  27. package/ios/PdfViewerView.swift +268 -0
  28. package/ios/PdfViewerViewController.swift +109 -0
  29. package/lib/commonjs/PdfViewerView.js +105 -0
  30. package/lib/commonjs/PdfViewerView.js.map +1 -0
  31. package/lib/commonjs/index.js +28 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/package.json +1 -0
  34. package/lib/commonjs/specs/NativePdfViewerComponent.js +27 -0
  35. package/lib/commonjs/specs/NativePdfViewerComponent.js.map +1 -0
  36. package/lib/commonjs/specs/NativePdfViewerModule.js +21 -0
  37. package/lib/commonjs/specs/NativePdfViewerModule.js.map +1 -0
  38. package/lib/commonjs/usePdfViewer.js +65 -0
  39. package/lib/commonjs/usePdfViewer.js.map +1 -0
  40. package/lib/module/PdfViewerView.js +99 -0
  41. package/lib/module/PdfViewerView.js.map +1 -0
  42. package/lib/module/index.js +8 -0
  43. package/lib/module/index.js.map +1 -0
  44. package/lib/module/package.json +1 -0
  45. package/lib/module/specs/NativePdfViewerComponent.js +26 -0
  46. package/lib/module/specs/NativePdfViewerComponent.js.map +1 -0
  47. package/lib/module/specs/NativePdfViewerModule.js +18 -0
  48. package/lib/module/specs/NativePdfViewerModule.js.map +1 -0
  49. package/lib/module/usePdfViewer.js +60 -0
  50. package/lib/module/usePdfViewer.js.map +1 -0
  51. package/lib/typescript/PdfViewerView.d.ts +58 -0
  52. package/lib/typescript/PdfViewerView.d.ts.map +1 -0
  53. package/lib/typescript/index.d.ts +7 -0
  54. package/lib/typescript/index.d.ts.map +1 -0
  55. package/lib/typescript/specs/NativePdfViewerComponent.d.ts +59 -0
  56. package/lib/typescript/specs/NativePdfViewerComponent.d.ts.map +1 -0
  57. package/lib/typescript/specs/NativePdfViewerModule.d.ts +47 -0
  58. package/lib/typescript/specs/NativePdfViewerModule.d.ts.map +1 -0
  59. package/lib/typescript/usePdfViewer.d.ts +45 -0
  60. package/lib/typescript/usePdfViewer.d.ts.map +1 -0
  61. package/package.json +109 -0
  62. package/react-native-pdfrender.podspec +35 -0
  63. package/react-native.config.js +11 -0
  64. package/src/PdfViewerView.tsx +159 -0
  65. package/src/index.tsx +10 -0
  66. package/src/specs/NativePdfViewerComponent.ts +94 -0
  67. package/src/specs/NativePdfViewerModule.ts +58 -0
  68. package/src/usePdfViewer.ts +102 -0
@@ -0,0 +1,38 @@
1
+ import UIKit
2
+
3
+ // MARK: - Scroll direction
4
+
5
+ enum PdfScrollDirection: String {
6
+ case vertical
7
+ case horizontal
8
+ }
9
+
10
+ // MARK: - Shared constants
11
+
12
+ enum PdfConstants {
13
+ /// Minimum allowed zoom scale (50%)
14
+ static let minZoom: CGFloat = 0.5
15
+ /// Maximum allowed zoom scale (500%)
16
+ static let maxZoom: CGFloat = 5.0
17
+ /// Default initial zoom (150%)
18
+ static let defaultZoom: CGFloat = 1.5
19
+ /// Vertical gap between consecutive pages
20
+ static let pageSpacing: CGFloat = 12.0
21
+ /// Horizontal padding around each page
22
+ static let pageHorizontalPadding: CGFloat = 16.0
23
+ /// Multiplier applied to target width before rendering.
24
+ /// Retina screen (2×) × 1.5 = 3× physical pixels → crisp at maximum zoom.
25
+ static let renderScale: CGFloat = UIScreen.main.scale * 1.5
26
+ /// Max number of page images kept in NSCache
27
+ static let cacheCountLimit = 20
28
+ /// Max total bytes in NSCache (50 MB)
29
+ static let cacheTotalCostLimit = 50 * 1024 * 1024
30
+ /// Fixed height of the toolbar
31
+ static let toolbarHeight: CGFloat = 52.0
32
+ /// Corner radius applied to each page card
33
+ static let cornerRadius: CGFloat = 4.0
34
+ /// Drop-shadow opacity on each page card
35
+ static let pageShadowOpacity: Float = 0.15
36
+ /// Step factor for each tap of + / – zoom button
37
+ static let zoomStep: CGFloat = 1.25
38
+ }
@@ -0,0 +1,121 @@
1
+ import UIKit
2
+
3
+ /// UIView that displays a single rendered PDF page.
4
+ ///
5
+ /// Rendering is always performed on a background queue; the result is
6
+ /// cached in PdfCacheManager and applied on the main thread.
7
+ /// Cancelling an in-flight render is safe — the DispatchWorkItem is
8
+ /// checked for cancellation before the result is applied.
9
+ final class PdfPageView: UIView {
10
+
11
+ // MARK: - Public
12
+
13
+ /// 0-based index of the page this view represents.
14
+ var pageIndex: Int = 0
15
+
16
+ // MARK: - Private
17
+
18
+ private let imageView = UIImageView()
19
+ private let spinner = UIActivityIndicatorView(style: .medium)
20
+ private var renderTask: DispatchWorkItem?
21
+
22
+ // MARK: - Init
23
+
24
+ override init(frame: CGRect) {
25
+ super.init(frame: frame)
26
+ setup()
27
+ }
28
+
29
+ required init?(coder: NSCoder) { fatalError("Use init(frame:)") }
30
+
31
+ // MARK: - Setup
32
+
33
+ private func setup() {
34
+ backgroundColor = .white
35
+ clipsToBounds = true
36
+
37
+ imageView.contentMode = .scaleAspectFit
38
+ imageView.backgroundColor = .white
39
+ imageView.translatesAutoresizingMaskIntoConstraints = false
40
+ addSubview(imageView)
41
+
42
+ spinner.translatesAutoresizingMaskIntoConstraints = false
43
+ addSubview(spinner)
44
+
45
+ NSLayoutConstraint.activate([
46
+ imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
47
+ imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
48
+ imageView.topAnchor.constraint(equalTo: topAnchor),
49
+ imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
50
+
51
+ spinner.centerXAnchor.constraint(equalTo: centerXAnchor),
52
+ spinner.centerYAnchor.constraint(equalTo: centerYAnchor),
53
+ ])
54
+ spinner.startAnimating()
55
+ }
56
+
57
+ // MARK: - Loading
58
+
59
+ /// Loads and renders the page at `pageIndex` using `document`.
60
+ ///
61
+ /// - Parameters:
62
+ /// - document: An open CGPDFDocument (kept alive by the caller).
63
+ /// - targetWidth: Logical width of the container; rendering happens
64
+ /// at `targetWidth × PdfConstants.renderScale` for retina.
65
+ func load(document: CGPDFDocument, targetWidth: CGFloat) {
66
+ let cacheKey = PdfCacheManager.key(pageIndex: pageIndex, targetWidth: targetWidth)
67
+
68
+ // Serve from cache immediately if available
69
+ if let cached = PdfCacheManager.shared.image(forKey: cacheKey) {
70
+ imageView.image = cached
71
+ spinner.stopAnimating()
72
+ return
73
+ }
74
+
75
+ // Cancel any previous render for this view
76
+ renderTask?.cancel()
77
+ renderTask = nil
78
+
79
+ // Capture locals so the closure doesn't hold self strongly during rendering
80
+ let idx = pageIndex
81
+ let renderWidth = targetWidth * PdfConstants.renderScale
82
+
83
+ var item: DispatchWorkItem!
84
+ item = DispatchWorkItem { [weak self] in
85
+ guard !item.isCancelled else { return }
86
+
87
+ let image = PdfRenderingLogic.renderPage(
88
+ document: document,
89
+ pageIndex: idx,
90
+ targetWidth: renderWidth
91
+ )
92
+
93
+ if let image {
94
+ PdfCacheManager.shared.setImage(image, forKey: cacheKey)
95
+ }
96
+
97
+ DispatchQueue.main.async { [weak self] in
98
+ guard let self, !item.isCancelled else { return }
99
+ self.imageView.image = image
100
+ self.spinner.stopAnimating()
101
+ }
102
+ }
103
+
104
+ renderTask = item
105
+ DispatchQueue.global(qos: .userInitiated).async(execute: item)
106
+ }
107
+
108
+ /// Cancels any in-flight render task.
109
+ func cancelLoad() {
110
+ renderTask?.cancel()
111
+ renderTask = nil
112
+ }
113
+
114
+ // MARK: - Reuse
115
+
116
+ func prepareForReuse() {
117
+ cancelLoad()
118
+ imageView.image = nil
119
+ spinner.startAnimating()
120
+ }
121
+ }
@@ -0,0 +1,107 @@
1
+ import UIKit
2
+ import CoreGraphics
3
+
4
+ /// Stateless PDF rendering utilities using CoreGraphics (CGPDFDocument).
5
+ ///
6
+ /// All rendering methods are safe to call from a background thread.
7
+ /// CGPDFDocument is thread-safe for concurrent reads once opened.
8
+ ///
9
+ /// Why CoreGraphics instead of PDFKit?
10
+ /// CGPDFDocument gives direct access to the page bitmap, letting us
11
+ /// control output resolution (PdfConstants.renderScale) and feed the
12
+ /// result into a custom UIScrollView. PDFKit's PDFView is great for
13
+ /// simple embed cases but harder to customise for split-view or
14
+ /// controlled-zoom layouts.
15
+ enum PdfRenderingLogic {
16
+
17
+ // MARK: - Document opening
18
+
19
+ /// Opens a CGPDFDocument from a file URL.
20
+ /// Returns nil if the URL is unreachable or the file is not a valid PDF.
21
+ static func openDocument(url: URL) -> CGPDFDocument? {
22
+ CGPDFDocument(url as CFURL)
23
+ }
24
+
25
+ /// Convenience: opens a document from a string that may be either a
26
+ /// file:// URI or a plain filesystem path.
27
+ static func openDocument(uri: String) -> CGPDFDocument? {
28
+ openDocument(url: fileURL(from: uri))
29
+ }
30
+
31
+ // MARK: - Page count
32
+
33
+ /// Synchronously returns total page count without rendering any content.
34
+ /// Safe to call from any thread.
35
+ static func pageCount(url: URL) -> Int {
36
+ CGPDFDocument(url as CFURL)?.numberOfPages ?? 0
37
+ }
38
+
39
+ static func pageCount(uri: String) -> Int {
40
+ pageCount(url: fileURL(from: uri))
41
+ }
42
+
43
+ // MARK: - Page rendering
44
+
45
+ /// Renders a single PDF page to a UIImage.
46
+ ///
47
+ /// - Parameters:
48
+ /// - document: Open CGPDFDocument.
49
+ /// - pageIndex: 0-based page index.
50
+ /// - targetWidth: Desired pixel width of the output image.
51
+ /// Pass `view.bounds.width * PdfConstants.renderScale`
52
+ /// for retina-quality output.
53
+ /// - Returns: Rendered UIImage, or nil if pageIndex is out of range.
54
+ static func renderPage(
55
+ document: CGPDFDocument,
56
+ pageIndex: Int,
57
+ targetWidth: CGFloat
58
+ ) -> UIImage? {
59
+ // CGPDFDocument is 1-based
60
+ guard
61
+ pageIndex >= 0,
62
+ pageIndex < document.numberOfPages,
63
+ let page = document.page(at: pageIndex + 1)
64
+ else { return nil }
65
+
66
+ let mediaBox = page.getBoxRect(.mediaBox)
67
+ guard mediaBox.width > 0 else { return nil }
68
+
69
+ let scale = targetWidth / mediaBox.width
70
+ let size = CGSize(width: targetWidth, height: mediaBox.height * scale)
71
+
72
+ let renderer = UIGraphicsImageRenderer(size: size)
73
+ return renderer.image { ctx in
74
+ let cgCtx = ctx.cgContext
75
+
76
+ // White background — PDFs may have transparent backgrounds
77
+ cgCtx.setFillColor(UIColor.white.cgColor)
78
+ cgCtx.fill(CGRect(origin: .zero, size: size))
79
+
80
+ // CGPDFPage origin is bottom-left; UIKit origin is top-left.
81
+ // Flip the coordinate system vertically before drawing.
82
+ cgCtx.translateBy(x: 0, y: size.height)
83
+ cgCtx.scaleBy(x: scale, y: -scale)
84
+ cgCtx.drawPDFPage(page)
85
+ }
86
+ }
87
+
88
+ // MARK: - Aspect ratio
89
+
90
+ /// Returns width/height aspect ratio for a given page (for layout).
91
+ /// Falls back to A4 ratio (0.707) if the page cannot be opened.
92
+ static func pageAspectRatio(document: CGPDFDocument, pageIndex: Int) -> CGFloat {
93
+ guard let page = document.page(at: pageIndex + 1) else { return 0.707 }
94
+ let box = page.getBoxRect(.mediaBox)
95
+ return box.height > 0 ? box.width / box.height : 0.707
96
+ }
97
+
98
+ // MARK: - URI helpers
99
+
100
+ /// Converts a string that is either a file:// URI or a plain path to URL.
101
+ static func fileURL(from uri: String) -> URL {
102
+ if uri.hasPrefix("file://") {
103
+ return URL(string: uri) ?? URL(fileURLWithPath: uri)
104
+ }
105
+ return URL(fileURLWithPath: uri)
106
+ }
107
+ }
@@ -0,0 +1,158 @@
1
+ import UIKit
2
+
3
+ // MARK: - Delegate
4
+
5
+ protocol PdfToolbarViewDelegate: AnyObject {
6
+ func toolbarDidTapBack(_ toolbar: PdfToolbarView)
7
+ func toolbarDidTapZoomIn(_ toolbar: PdfToolbarView)
8
+ func toolbarDidTapZoomOut(_ toolbar: PdfToolbarView)
9
+ func toolbarDidTapFullScreen(_ toolbar: PdfToolbarView)
10
+ }
11
+
12
+ // MARK: - PdfToolbarView
13
+
14
+ /// A fixed-height toolbar that mirrors the Android PdfViewerToolbar behaviour.
15
+ ///
16
+ /// Layout (left → right):
17
+ /// [← Back] [spacer] [Title] [spacer] [−] [+] [⛶]
18
+ final class PdfToolbarView: UIView {
19
+
20
+ // MARK: Public
21
+
22
+ weak var delegate: PdfToolbarViewDelegate?
23
+
24
+ var isFullScreen: Bool = false {
25
+ didSet {
26
+ fullScreenButton.setTitle(isFullScreen ? "⊡" : "⛶", for: .normal)
27
+ fullScreenButton.backgroundColor = isFullScreen
28
+ ? UIColor(red: 1.0, green: 0.93, blue: 0.93, alpha: 1.0)
29
+ : UIColor(red: 0.93, green: 0.96, blue: 1.0, alpha: 1.0)
30
+ }
31
+ }
32
+
33
+ var headerText: String? {
34
+ didSet { titleLabel.text = headerText }
35
+ }
36
+
37
+ var backButtonText: String? {
38
+ didSet { backButton.setTitle(backButtonText ?? "Back", for: .normal) }
39
+ }
40
+
41
+ // Custom icon images (optional — falls back to text glyphs when nil)
42
+ var zoomInImage: UIImage? { didSet { applyIcon(zoomInImage, to: zoomInButton, fallback: "+") } }
43
+ var zoomOutImage: UIImage? { didSet { applyIcon(zoomOutImage, to: zoomOutButton, fallback: "−") } }
44
+ var fullScreenImage: UIImage? { didSet { applyIcon(fullScreenImage, to: fullScreenButton, fallback: "⛶") } }
45
+
46
+ // MARK: - Private UI
47
+
48
+ private let backButton = UIButton(type: .system)
49
+ private let titleLabel = UILabel()
50
+ private let zoomOutButton = UIButton(type: .system)
51
+ private let zoomInButton = UIButton(type: .system)
52
+ private let fullScreenButton = UIButton(type: .system)
53
+
54
+ // MARK: - Init
55
+
56
+ override init(frame: CGRect) {
57
+ super.init(frame: frame)
58
+ setup()
59
+ }
60
+
61
+ required init?(coder: NSCoder) { fatalError("Use init(frame:)") }
62
+
63
+ // MARK: - Setup
64
+
65
+ private func setup() {
66
+ backgroundColor = .white
67
+
68
+ // Top border line instead of shadow
69
+ let separator = UIView()
70
+ separator.backgroundColor = UIColor(white: 0.88, alpha: 1)
71
+ separator.translatesAutoresizingMaskIntoConstraints = false
72
+ addSubview(separator)
73
+ NSLayoutConstraint.activate([
74
+ separator.topAnchor.constraint(equalTo: topAnchor),
75
+ separator.leadingAnchor.constraint(equalTo: leadingAnchor),
76
+ separator.trailingAnchor.constraint(equalTo: trailingAnchor),
77
+ separator.heightAnchor.constraint(equalToConstant: 0.5),
78
+ ])
79
+
80
+ // Back button
81
+ backButton.setTitle("Back", for: .normal)
82
+ backButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
83
+ backButton.setTitleColor(UIColor.systemBlue, for: .normal)
84
+ backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
85
+ backButton.setContentHuggingPriority(.required, for: .horizontal)
86
+
87
+ // Title
88
+ titleLabel.text = ""
89
+ titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
90
+ titleLabel.textColor = UIColor(red: 0.10, green: 0.10, blue: 0.18, alpha: 1)
91
+ titleLabel.textAlignment = .center
92
+ titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
93
+
94
+ // Zoom out — styled icon button
95
+ styleIconButton(zoomOutButton, symbol: "−", fontSize: 26)
96
+ zoomOutButton.addTarget(self, action: #selector(zoomOutTapped), for: .touchUpInside)
97
+
98
+ // Zoom in — styled icon button
99
+ styleIconButton(zoomInButton, symbol: "+", fontSize: 26)
100
+ zoomInButton.addTarget(self, action: #selector(zoomInTapped), for: .touchUpInside)
101
+
102
+ // Full-screen toggle — styled icon button
103
+ styleIconButton(fullScreenButton, symbol: "⛶", fontSize: 22)
104
+ fullScreenButton.addTarget(self, action: #selector(fullScreenTapped), for: .touchUpInside)
105
+
106
+ let stack = UIStackView(arrangedSubviews: [
107
+ backButton,
108
+ UIView(), // flexible spacer
109
+ titleLabel,
110
+ UIView(), // flexible spacer
111
+ zoomOutButton,
112
+ zoomInButton,
113
+ fullScreenButton,
114
+ ])
115
+ stack.axis = .horizontal
116
+ stack.spacing = 12
117
+ stack.alignment = .center
118
+ stack.translatesAutoresizingMaskIntoConstraints = false
119
+ addSubview(stack)
120
+
121
+ NSLayoutConstraint.activate([
122
+ stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
123
+ stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
124
+ stack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
125
+ stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
126
+ ])
127
+ }
128
+
129
+ private func styleIconButton(_ button: UIButton, symbol: String, fontSize: CGFloat) {
130
+ button.setTitle(symbol, for: .normal)
131
+ button.titleLabel?.font = .systemFont(ofSize: fontSize, weight: .medium)
132
+ button.setTitleColor(.systemBlue, for: .normal)
133
+ button.setTitleColor(UIColor.systemBlue.withAlphaComponent(0.4), for: .disabled)
134
+ button.backgroundColor = UIColor(red: 0.93, green: 0.96, blue: 1.0, alpha: 1.0)
135
+ button.layer.cornerRadius = 8
136
+ button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
137
+ button.setContentHuggingPriority(.required, for: .horizontal)
138
+ }
139
+
140
+ // MARK: - Icon helpers
141
+
142
+ private func applyIcon(_ image: UIImage?, to button: UIButton, fallback: String) {
143
+ if let image {
144
+ button.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
145
+ button.setTitle(nil, for: .normal)
146
+ } else {
147
+ button.setImage(nil, for: .normal)
148
+ button.setTitle(fallback, for: .normal)
149
+ }
150
+ }
151
+
152
+ // MARK: - Actions
153
+
154
+ @objc private func backTapped() { delegate?.toolbarDidTapBack(self) }
155
+ @objc private func zoomInTapped() { delegate?.toolbarDidTapZoomIn(self) }
156
+ @objc private func zoomOutTapped() { delegate?.toolbarDidTapZoomOut(self) }
157
+ @objc private func fullScreenTapped() { delegate?.toolbarDidTapFullScreen(self) }
158
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * PdfViewerComponentView.mm — Fabric ViewManager for PdfViewerView.
3
+ *
4
+ * Architecture
5
+ * ────────────
6
+ * Swift class (PdfViewerView.swift) ← all rendering logic
7
+ * This ObjC++ file ← Fabric integration layer
8
+ * ├─ Hosts Swift PdfViewerView as contentView
9
+ * ├─ Receives props from codegen-generated PdfViewerViewProps C++ struct
10
+ * └─ Dispatches events via codegen-generated PdfViewerViewEventEmitter
11
+ *
12
+ * Codegen source: src/specs/NativePdfViewerComponent.ts
13
+ * Codegen output: build/generated/ios/PdfViewerSpecs/
14
+ * ├─ ComponentDescriptors.h (PdfViewerViewComponentDescriptor)
15
+ * ├─ Props.h (PdfViewerViewProps)
16
+ * ├─ EventEmitters.h (PdfViewerViewEventEmitter)
17
+ * └─ RCTComponentViewHelpers.h (RCTPdfViewerViewViewProtocol)
18
+ *
19
+ * Why RCTViewComponentView instead of RCTViewManager?
20
+ * RCTViewManager is the old-arch pattern. In Fabric, each component is a
21
+ * self-contained RCTViewComponentView that receives C++ props directly via
22
+ * JSI — no bridge serialization, no main-queue hopping for prop delivery.
23
+ *
24
+ * Guard: compiled only when New Architecture is enabled.
25
+ */
26
+
27
+ #ifdef RCT_NEW_ARCH_ENABLED
28
+
29
+ #import <React/RCTViewComponentView.h>
30
+ #import <UIKit/UIKit.h>
31
+
32
+ // ── Forward-declare Swift PdfViewerView ───────────────────────────────────────
33
+ // Declare only the properties and block callbacks used in this translation unit.
34
+ // The actual UIView subclass is resolved by the linker from PdfViewerView.swift.
35
+
36
+ @interface PdfViewerView : UIView
37
+
38
+ // Props
39
+ @property (nonatomic, copy, nullable) NSString *pdfUri;
40
+ @property (nonatomic) NSInteger initialPageIndex;
41
+ @property (nonatomic) CGFloat defaultZoom;
42
+ @property (nonatomic) BOOL isFullScreen;
43
+ @property (nonatomic, copy, nullable) NSString *headerText;
44
+ @property (nonatomic, copy, nullable) NSString *backButtonText;
45
+
46
+ // Event callbacks (set by this manager; called by PdfViewerView when events fire)
47
+ @property (nonatomic, copy, nullable) void (^onFullScreenChangeBlock)(BOOL);
48
+ @property (nonatomic, copy, nullable) void (^onZoomChangeBlock)(float);
49
+ @property (nonatomic, copy, nullable) void (^onLeftScreenChangeBlock)(BOOL);
50
+ @property (nonatomic, copy, nullable) void (^onRightScreenChangeBlock)(BOOL);
51
+
52
+ @end
53
+
54
+ // ── Codegen-generated headers ─────────────────────────────────────────────────
55
+ // Generated by `yarn ios` / `pod install` from src/specs/NativePdfViewerComponent.ts
56
+ // The correct include path mirrors the ReactCodegen pod's directory layout:
57
+ // <react/renderer/components/<spec-name>/<header>.h>
58
+ // This matches how react-native-safe-area-context and other Fabric components import them.
59
+
60
+ #import <react/renderer/components/PdfViewerSpecs/ComponentDescriptors.h>
61
+ #import <react/renderer/components/PdfViewerSpecs/EventEmitters.h>
62
+ #import <react/renderer/components/PdfViewerSpecs/Props.h>
63
+ #import <react/renderer/components/PdfViewerSpecs/RCTComponentViewHelpers.h>
64
+
65
+ #import <React/RCTFabricComponentsPlugins.h>
66
+
67
+ using namespace facebook::react;
68
+
69
+ // ── PdfViewerComponentView ────────────────────────────────────────────────────
70
+
71
+ @interface PdfViewerComponentView : RCTViewComponentView <RCTPdfViewerViewViewProtocol>
72
+ @end
73
+
74
+ @implementation PdfViewerComponentView {
75
+ PdfViewerView *_pdfView;
76
+ }
77
+
78
+ // ── Fabric registration ───────────────────────────────────────────────────────
79
+
80
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
81
+ return concreteComponentDescriptorProvider<PdfViewerViewComponentDescriptor>();
82
+ }
83
+
84
+ // ── Initialisation ────────────────────────────────────────────────────────────
85
+
86
+ - (instancetype)initWithFrame:(CGRect)frame {
87
+ if (self = [super initWithFrame:frame]) {
88
+ _pdfView = [[PdfViewerView alloc] initWithFrame:frame];
89
+
90
+ __weak PdfViewerComponentView *weakSelf = self;
91
+
92
+ // Wire each Swift block to the Fabric event emitter.
93
+ // The event payload field names must match the TS spec interfaces.
94
+
95
+ _pdfView.onFullScreenChangeBlock = ^(BOOL isFS) {
96
+ auto strongSelf = weakSelf;
97
+ if (!strongSelf || !strongSelf->_eventEmitter) return;
98
+ auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
99
+ strongSelf->_eventEmitter
100
+ );
101
+ if (emitter) {
102
+ emitter->onFullScreenChange(
103
+ PdfViewerViewEventEmitter::OnFullScreenChange{.isFullScreen = (bool)isFS}
104
+ );
105
+ }
106
+ };
107
+
108
+ _pdfView.onZoomChangeBlock = ^(float zoom) {
109
+ auto strongSelf = weakSelf;
110
+ if (!strongSelf || !strongSelf->_eventEmitter) return;
111
+ auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
112
+ strongSelf->_eventEmitter
113
+ );
114
+ if (emitter) {
115
+ emitter->onZoomChange(
116
+ PdfViewerViewEventEmitter::OnZoomChange{.zoomPercentage = zoom}
117
+ );
118
+ }
119
+ };
120
+
121
+ _pdfView.onLeftScreenChangeBlock = ^(BOOL isLeft) {
122
+ auto strongSelf = weakSelf;
123
+ if (!strongSelf || !strongSelf->_eventEmitter) return;
124
+ auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
125
+ strongSelf->_eventEmitter
126
+ );
127
+ if (emitter) {
128
+ emitter->onLeftScreenChange(
129
+ PdfViewerViewEventEmitter::OnLeftScreenChange{.isLeftScreen = (bool)isLeft}
130
+ );
131
+ }
132
+ };
133
+
134
+ _pdfView.onRightScreenChangeBlock = ^(BOOL isRight) {
135
+ auto strongSelf = weakSelf;
136
+ if (!strongSelf || !strongSelf->_eventEmitter) return;
137
+ auto emitter = std::dynamic_pointer_cast<const PdfViewerViewEventEmitter>(
138
+ strongSelf->_eventEmitter
139
+ );
140
+ if (emitter) {
141
+ emitter->onRightScreenChange(
142
+ PdfViewerViewEventEmitter::OnRightScreenChange{.isRightScreen = (bool)isRight}
143
+ );
144
+ }
145
+ };
146
+
147
+ self.contentView = _pdfView;
148
+ }
149
+ return self;
150
+ }
151
+
152
+ // ── Prop delivery (called by Fabric on every JS prop update) ──────────────────
153
+
154
+ - (void)updateProps:(const Props::Shared &)props
155
+ oldProps:(const Props::Shared &)oldProps
156
+ {
157
+ const auto &p = *std::static_pointer_cast<const PdfViewerViewProps>(props);
158
+
159
+ // pdfUri
160
+ if (!p.pdfUri.empty()) {
161
+ NSString *uri = [NSString stringWithUTF8String:p.pdfUri.c_str()];
162
+ if (![_pdfView.pdfUri isEqualToString:uri]) {
163
+ _pdfView.pdfUri = uri;
164
+ }
165
+ }
166
+
167
+ // Numeric / bool props
168
+ _pdfView.initialPageIndex = p.initialPageIndex;
169
+ _pdfView.defaultZoom = (CGFloat)p.defaultZoom;
170
+ _pdfView.isFullScreen = p.isFullScreen;
171
+
172
+ // Optional string props
173
+ if (!p.headerText.empty()) {
174
+ _pdfView.headerText = [NSString stringWithUTF8String:p.headerText.c_str()];
175
+ }
176
+ if (!p.backButtonText.empty()) {
177
+ _pdfView.backButtonText = [NSString stringWithUTF8String:p.backButtonText.c_str()];
178
+ }
179
+
180
+ [super updateProps:props oldProps:oldProps];
181
+ }
182
+
183
+ @end
184
+
185
+ // ── Plugin registration ───────────────────────────────────────────────────────
186
+ // Fabric uses this C function to look up the component view class by name.
187
+ // The name "PdfViewerView" must match codegenNativeComponent('PdfViewerView')
188
+ // in NativePdfViewerComponent.ts AND VIEW_NAME returned by the Android manager.
189
+
190
+ Class<RCTComponentViewProtocol> PdfViewerViewCls(void) {
191
+ return PdfViewerComponentView.class;
192
+ }
193
+
194
+ #endif // RCT_NEW_ARCH_ENABLED