react-native-pdf-jsi 4.1.1 → 4.2.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.
package/PdfView.js CHANGED
@@ -83,6 +83,7 @@ export default class PdfView extends Component {
83
83
  scale: this.props.scale,
84
84
  contentOffset: {x: 0, y: 0},
85
85
  newContentOffset: {x: 0, y: 0},
86
+ previousPage: -1, // Track previous page to prevent unnecessary navigations
86
87
  };
87
88
 
88
89
  this._flatList = null;
@@ -121,10 +122,16 @@ export default class PdfView extends Component {
121
122
  this.props.onError(error);
122
123
  });
123
124
 
125
+ // Initialize page navigation after PDF loads
124
126
  clearTimeout(this._scrollTimer);
125
127
  this._scrollTimer = setTimeout(() => {
126
- if (this._flatList) {
127
- this._flatList.scrollToIndex({animated: false, index: this.props.page < 1 ? 0 : this.props.page - 1});
128
+ if (this._flatList && this._mounted) {
129
+ const initialPage = this.props.page < 1 ? 0 : this.props.page - 1;
130
+ this._flatList.scrollToIndex({
131
+ animated: false,
132
+ index: initialPage
133
+ });
134
+ this.setState({ previousPage: this.props.page < 1 ? 1 : this.props.page });
128
135
  }
129
136
  }, 200);
130
137
  }
@@ -139,15 +146,30 @@ export default class PdfView extends Component {
139
146
  });
140
147
  }
141
148
 
142
- if (this.props.horizontal !== prevProps.horizontal || this.props.page !== prevProps.page) {
149
+ // Only navigate if page actually changed and PDF is loaded
150
+ const pageChanged = this.props.page !== prevProps.page;
151
+ const horizontalChanged = this.props.horizontal !== prevProps.horizontal;
152
+
153
+ if ((horizontalChanged || pageChanged) && this.state.pdfLoaded && this._flatList) {
143
154
  let page = (this.props.page) < 1 ? 1 : this.props.page;
144
155
  page = page > this.state.numberOfPages ? this.state.numberOfPages : page;
145
-
146
- if (this._flatList) {
156
+
157
+ // Only navigate if page actually changed from previous navigation
158
+ if (page !== this.state.previousPage) {
147
159
  clearTimeout(this._scrollTimer);
148
160
  this._scrollTimer = setTimeout(() => {
149
- this._flatList.scrollToIndex({animated: false, index: page - 1});
150
- }, 200);
161
+ if (this._flatList && this._mounted) {
162
+ // Use animated: true for smooth transitions when pagination is disabled
163
+ // Use animated: false only when paging is enabled for instant snap
164
+ const shouldAnimate = !this.props.enablePaging;
165
+ this._flatList.scrollToIndex({
166
+ animated: shouldAnimate,
167
+ index: page - 1,
168
+ viewPosition: 0.5 // Center the page in view
169
+ });
170
+ this.setState({ previousPage: page });
171
+ }
172
+ }, horizontalChanged ? 300 : 100); // Longer delay if orientation changed
151
173
  }
152
174
  }
153
175
 
@@ -339,7 +361,10 @@ export default class PdfView extends Component {
339
361
  let data = [];
340
362
 
341
363
  if (this.props.singlePage) {
342
- data[0] = {key: this.props.currentPage >= 0 ? this.props.currentPage : 0}
364
+ // Fix: Use page prop instead of currentPage for singlePage mode
365
+ // This allows the page prop to control which page is displayed
366
+ const pageToShow = this.props.page >= 1 ? this.props.page - 1 : 0;
367
+ data[0] = {key: pageToShow};
343
368
  } else {
344
369
  for (let i = 0; i < this.state.numberOfPages; i++) {
345
370
  data[i] = {key: i};
@@ -362,6 +387,9 @@ export default class PdfView extends Component {
362
387
  windowSize={11}
363
388
  getItemLayout={this._getItemLayout}
364
389
  maxToRenderPerBatch={1}
390
+ // Prevent full rerenders when page changes - only rerender when data actually changes
391
+ extraData={this.props.singlePage ? this.props.page : this.state.numberOfPages}
392
+ removeClippedSubviews={true}
365
393
  renderScrollComponent={(props) => <ScrollView
366
394
  {...props}
367
395
  centerContent={this.state.centerContent}
package/PinchZoomView.js CHANGED
@@ -6,120 +6,95 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
 
9
- 'use strict';
10
- import React, {Component} from 'react';
11
- import PropTypes from 'prop-types';
12
- import {
13
- View,
14
- StyleSheet,
15
- PanResponder
16
- } from 'react-native';
17
- import {ViewPropTypes} from 'deprecated-react-native-prop-types';
9
+ "use strict";
10
+ import React, { Component } from "react";
11
+ import PropTypes from "prop-types";
12
+ import { View, StyleSheet, PanResponder } from "react-native";
13
+ import { ViewPropTypes } from "deprecated-react-native-prop-types";
18
14
  export default class PinchZoomView extends Component {
19
-
20
- static propTypes = {
21
- ...ViewPropTypes,
22
- scalable: PropTypes.bool,
23
- onScaleChanged: PropTypes.func,
24
- };
25
-
26
- static defaultProps = {
27
- scalable: true,
28
- onScaleChanged: (scale) => {
29
- },
30
- };
31
-
32
- constructor(props) {
33
-
34
- super(props);
35
- this.state = {};
36
- this.distant = 0;
37
- this.gestureHandlers = PanResponder.create({
38
- onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
39
- onMoveShouldSetResponderCapture: (evt, gestureState) => (true),
40
- onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
41
- onPanResponderGrant: this._handlePanResponderGrant,
42
- onPanResponderMove: this._handlePanResponderMove,
43
- onPanResponderRelease: this._handlePanResponderEnd,
44
- onPanResponderTerminationRequest: evt => false,
45
- onPanResponderTerminate: this._handlePanResponderTerminate,
46
- onShouldBlockNativeResponder: evt => true
47
- });
48
-
15
+ static propTypes = {
16
+ ...ViewPropTypes,
17
+ scalable: PropTypes.bool,
18
+ onScaleChanged: PropTypes.func,
19
+ };
20
+
21
+ static defaultProps = {
22
+ scalable: true,
23
+ onScaleChanged: (scale) => {},
24
+ };
25
+
26
+ constructor(props) {
27
+ super(props);
28
+ this.state = {};
29
+ this.distant = 0;
30
+ this.gestureHandlers = PanResponder.create({
31
+ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
32
+ onMoveShouldSetResponderCapture: (evt, gestureState) => {
33
+ return this.props.scalable && (evt.nativeEvent.touches.length >= 2 || gestureState.numberActiveTouches >= 2);
34
+ },
35
+ onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
36
+ onPanResponderGrant: this._handlePanResponderGrant,
37
+ onPanResponderMove: this._handlePanResponderMove,
38
+ onPanResponderRelease: this._handlePanResponderEnd,
39
+ onPanResponderTerminationRequest: (evt) => false,
40
+ onPanResponderTerminate: this._handlePanResponderTerminate,
41
+ onShouldBlockNativeResponder: (evt) => true,
42
+ });
43
+ }
44
+
45
+ _handleStartShouldSetPanResponder = (e, gestureState) => {
46
+ // don't respond to single touch to avoid shielding click on child components
47
+ return false;
48
+ };
49
+
50
+ _handleMoveShouldSetPanResponder = (e, gestureState) => {
51
+ return this.props.scalable && (e.nativeEvent.touches.length >= 2 || gestureState.numberActiveTouches >= 2);
52
+ };
53
+
54
+ _handlePanResponderGrant = (e, gestureState) => {
55
+ if (e.nativeEvent.touches.length >= 2 || gestureState.numberActiveTouches >= 2) {
56
+ let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
57
+ let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
58
+ this.distant = Math.sqrt(dx * dx + dy * dy);
49
59
  }
50
-
51
- _handleStartShouldSetPanResponder = (e, gestureState) => {
52
-
53
- // don't respond to single touch to avoid shielding click on child components
54
- return false;
55
-
56
- };
57
-
58
- _handleMoveShouldSetPanResponder = (e, gestureState) => {
59
-
60
- return this.props.scalable && (e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2);
61
-
62
- };
63
-
64
- _handlePanResponderGrant = (e, gestureState) => {
65
-
66
- if (e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2) {
67
- let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
68
- let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
69
- this.distant = Math.sqrt(dx * dx + dy * dy);
70
- }
71
-
72
- };
73
-
74
- _handlePanResponderEnd = (e, gestureState) => {
75
-
76
- this.distant = 0;
77
-
78
- };
79
-
80
- _handlePanResponderTerminate = (e, gestureState) => {
81
-
82
- this.distant = 0;
83
-
84
- };
85
-
86
- _handlePanResponderMove = (e, gestureState) => {
87
-
88
- if ((e.nativeEvent.changedTouches.length >= 2 || gestureState.numberActiveTouches >= 2) && this.distant > 100) {
89
-
90
- let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
91
- let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
92
- let distant = Math.sqrt(dx * dx + dy * dy);
93
- let scale = (distant / this.distant);
94
- let pageX = (e.nativeEvent.touches[0].pageX + e.nativeEvent.touches[1].pageX) / 2;
95
- let pageY = (e.nativeEvent.touches[0].pageY + e.nativeEvent.touches[1].pageY) / 2;
96
- let pinchInfo = {scale: scale, pageX: pageX, pageY: pageY};
97
-
98
- this.props.onScaleChanged(pinchInfo);
99
- this.distant = distant;
100
-
101
- }
102
-
103
- };
104
-
105
- render() {
106
-
107
- return (
108
- <View
109
- {...this.props}
110
- {...this.gestureHandlers?.panHandlers}
111
- style={[styles.container, this.props.style]}>
112
- {this.props.children}
113
- </View>
114
- );
115
-
60
+ };
61
+
62
+ _handlePanResponderEnd = (e, gestureState) => {
63
+ this.distant = 0;
64
+ };
65
+
66
+ _handlePanResponderTerminate = (e, gestureState) => {
67
+ this.distant = 0;
68
+ };
69
+
70
+ _handlePanResponderMove = (e, gestureState) => {
71
+ if ((e.nativeEvent.touches.length >= 2 || gestureState.numberActiveTouches >= 2) && this.distant > 100) {
72
+ let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX);
73
+ let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY);
74
+ let distant = Math.sqrt(dx * dx + dy * dy);
75
+ let scale = distant / this.distant;
76
+ let pageX = (e.nativeEvent.touches[0].pageX + e.nativeEvent.touches[1].pageX) / 2;
77
+ let pageY = (e.nativeEvent.touches[0].pageY + e.nativeEvent.touches[1].pageY) / 2;
78
+ let pinchInfo = { scale: scale, pageX: pageX, pageY: pageY };
79
+
80
+ this.props.onScaleChanged(pinchInfo);
81
+ this.distant = distant;
116
82
  }
83
+ };
84
+
85
+ render() {
86
+ return (
87
+ <View {...this.props} {...this.gestureHandlers?.panHandlers} style={[styles.container, this.props.style]}>
88
+ {this.props.children}
89
+ </View>
90
+ );
91
+ }
117
92
  }
118
93
 
119
94
  const styles = StyleSheet.create({
120
- container: {
121
- flex: 1,
122
- justifyContent: 'center',
123
- alignItems: 'center'
124
- }
95
+ container: {
96
+ flex: 1,
97
+ justifyContent: "center",
98
+ alignItems: "center",
99
+ },
125
100
  });
package/README.md CHANGED
@@ -410,6 +410,9 @@ MIT License - see [LICENSE](LICENSE) file for details.
410
410
 
411
411
  ## Recent Fixes
412
412
 
413
+ ### iOS Performance - Unnecessary Path Handlers (v4.2.0)
414
+ Fixed performance issue where path-related handlers were running unnecessarily when the path value hadn't actually changed. The fix filters out "path" from effectiveChangedProps when pathActuallyChanged=NO, preventing unnecessary reconfigurations of spacing, display direction, scroll views, usePageViewController, and other path-dependent handlers. This reduces unnecessary rerenders and improves performance, especially when navigating between pages. Addresses issue #7 (Page Prop Causes Full Rerender).
415
+
413
416
  ### iOS Pinch-to-Zoom (v4.1.1)
414
417
  Fixed critical issue where pinch-to-zoom gestures were not working on iOS. The fix removes interfering custom gesture recognizers and enables PDFView's native pinch-to-zoom functionality, which now works smoothly on both iOS and Android.
415
418
 
package/index.js CHANGED
@@ -681,6 +681,7 @@ export default class Pdf extends Component {
681
681
  {...this.props}
682
682
  style={[{backgroundColor: '#EEE',overflow: 'hidden'}, this.props.style]}
683
683
  path={this.state.path}
684
+ page={this.props.page}
684
685
  onLoadComplete={this.props.onLoadComplete}
685
686
  onPageChanged={this.props.onPageChanged}
686
687
  onError={this._onError}
@@ -54,6 +54,58 @@
54
54
  const float MAX_SCALE = 3.0f;
55
55
  const float MIN_SCALE = 1.0f;
56
56
 
57
+
58
+ @interface RNPDFScrollViewDelegateProxy : NSObject <UIScrollViewDelegate>
59
+ - (instancetype)initWithPrimary:(id<UIScrollViewDelegate>)primary secondary:(id<UIScrollViewDelegate>)secondary;
60
+ @end
61
+
62
+ @implementation RNPDFScrollViewDelegateProxy {
63
+ __weak id<UIScrollViewDelegate> _primary;
64
+ __weak id<UIScrollViewDelegate> _secondary;
65
+ }
66
+
67
+ - (instancetype)initWithPrimary:(id<UIScrollViewDelegate>)primary secondary:(id<UIScrollViewDelegate>)secondary {
68
+ if (self = [super init]) {
69
+ _primary = primary;
70
+ _secondary = secondary;
71
+ }
72
+ return self;
73
+ }
74
+
75
+ - (BOOL)respondsToSelector:(SEL)aSelector {
76
+ return [super respondsToSelector:aSelector]
77
+ || (_primary && [_primary respondsToSelector:aSelector])
78
+ || (_secondary && [_secondary respondsToSelector:aSelector]);
79
+ }
80
+
81
+ - (id)forwardingTargetForSelector:(SEL)aSelector {
82
+ if (_primary && [_primary respondsToSelector:aSelector]) {
83
+ return _primary;
84
+ }
85
+ if (_secondary && [_secondary respondsToSelector:aSelector]) {
86
+ return _secondary;
87
+ }
88
+ return [super forwardingTargetForSelector:aSelector];
89
+ }
90
+
91
+ - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
92
+ if (_primary && [_primary respondsToSelector:@selector(scrollViewDidScroll:)]) {
93
+ [_primary scrollViewDidScroll:scrollView];
94
+ }
95
+ if (_secondary && [_secondary respondsToSelector:@selector(scrollViewDidScroll:)]) {
96
+ [_secondary scrollViewDidScroll:scrollView];
97
+ }
98
+ }
99
+
100
+ - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
101
+ if (_primary && [_primary respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
102
+ return [_primary viewForZoomingInScrollView:scrollView];
103
+ }
104
+ return nil;
105
+ }
106
+
107
+ @end
108
+
57
109
  @interface RNPDFPdfView() <PDFDocumentDelegate, PDFViewDelegate
58
110
  #ifdef RCT_NEW_ARCH_ENABLED
59
111
  , RCTRNPDFPdfViewViewProtocol
@@ -68,6 +120,7 @@ const float MIN_SCALE = 1.0f;
68
120
  PDFView *_pdfView;
69
121
  UIScrollView *_internalScrollView;
70
122
  id<UIScrollViewDelegate> _originalScrollDelegate;
123
+ RNPDFScrollViewDelegateProxy *_scrollDelegateProxy;
71
124
  PDFOutline *root;
72
125
  float _fixScaleFactor;
73
126
  bool _initialed;
@@ -96,6 +149,15 @@ const float MIN_SCALE = 1.0f;
96
149
  NSMutableDictionary *_searchCache;
97
150
  NSString *_currentPdfId;
98
151
  NSOperationQueue *_preloadQueue;
152
+
153
+ // Page navigation state tracking
154
+ int _previousPage;
155
+ BOOL _isNavigating;
156
+ BOOL _documentLoaded;
157
+
158
+ // Track usePageViewController state to prevent unnecessary reconfiguration
159
+ BOOL _currentUsePageViewController;
160
+ BOOL _usePageViewControllerStateInitialized;
99
161
  }
100
162
 
101
163
  #ifdef RCT_NEW_ARCH_ENABLED
@@ -180,6 +242,7 @@ using namespace facebook::react;
180
242
  [updatedPropNames addObject:@"maxScale"];
181
243
  }
182
244
  if (_horizontal != newProps.horizontal) {
245
+ RCTLogInfo(@"🔄 [iOS Scroll] Horizontal prop changed: %d -> %d", _horizontal, newProps.horizontal);
183
246
  _horizontal = newProps.horizontal;
184
247
  [updatedPropNames addObject:@"horizontal"];
185
248
  }
@@ -212,6 +275,7 @@ using namespace facebook::react;
212
275
  [updatedPropNames addObject:@"password"];
213
276
  }
214
277
  if (_singlePage != newProps.singlePage) {
278
+ RCTLogInfo(@"🔄 [iOS Scroll] SinglePage prop changed: %d -> %d", _singlePage, newProps.singlePage);
215
279
  _singlePage = newProps.singlePage;
216
280
  [updatedPropNames addObject:@"singlePage"];
217
281
  }
@@ -273,6 +337,15 @@ using namespace facebook::react;
273
337
  _initialed = YES;
274
338
 
275
339
  [self didSetProps:mProps];
340
+
341
+ // Configure scroll view after layout to ensure it's found
342
+ // This is important because PDFKit creates the scroll view lazily
343
+ if (_documentLoaded && _pdfDocument) {
344
+ dispatch_async(dispatch_get_main_queue(), ^{
345
+ RCTLogInfo(@"🔍 [iOS Scroll] updateLayoutMetrics called, configuring scroll view after layout");
346
+ [self configureScrollView:self->_pdfView enabled:self->_scrollEnabled depth:0];
347
+ });
348
+ }
276
349
  }
277
350
 
278
351
  - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
@@ -316,6 +389,38 @@ using namespace facebook::react;
316
389
  _showsHorizontalScrollIndicator = YES;
317
390
  _showsVerticalScrollIndicator = YES;
318
391
  _scrollEnabled = YES;
392
+
393
+ // Initialize page navigation state
394
+ _previousPage = -1;
395
+ _isNavigating = NO;
396
+ _documentLoaded = NO;
397
+
398
+ // Initialize usePageViewController state tracking
399
+ _currentUsePageViewController = NO;
400
+ _usePageViewControllerStateInitialized = NO;
401
+
402
+ // #region agent log
403
+ {
404
+ NSString *logPath0 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
405
+ NSDictionary *logEntry0 = @{
406
+ @"sessionId": @"debug-session",
407
+ @"runId": @"init",
408
+ @"hypothesisId": @"F",
409
+ @"location": @"RNPDFPdfView.mm:393",
410
+ @"message": @"initCommonProps completed",
411
+ @"data": @{
412
+ @"horizontal": @(_horizontal),
413
+ @"enablePaging": @(_enablePaging),
414
+ @"scrollEnabled": @(_scrollEnabled),
415
+ @"singlePage": @(_singlePage)
416
+ },
417
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
418
+ };
419
+ NSData *logData0 = [NSJSONSerialization dataWithJSONObject:logEntry0 options:0 error:nil];
420
+ NSString *logLine0 = [[NSString alloc] initWithData:logData0 encoding:NSUTF8StringEncoding];
421
+ [[logLine0 stringByAppendingString:@"\n"] writeToFile:logPath0 atomically:YES encoding:NSUTF8StringEncoding error:nil];
422
+ }
423
+ // #endregion
319
424
 
320
425
  // Enhanced properties
321
426
  _enableCaching = YES;
@@ -387,13 +492,63 @@ using namespace facebook::react;
387
492
  _changedProps = changedProps;
388
493
 
389
494
  } else {
495
+ // Log all didSetProps calls to understand what's triggering reconfigurations
496
+ RCTLogInfo(@"📥 [iOS Scroll] didSetProps called - changedProps=%@, initialized=%d, currentUsePageVC=%d",
497
+ changedProps, _usePageViewControllerStateInitialized, _currentUsePageViewController);
390
498
 
391
- if ([changedProps containsObject:@"path"]) {
499
+ // Create filtered changedProps array - remove "path" if it hasn't actually changed
500
+ // This prevents unnecessary reconfigurations when path is in changedProps but value unchanged
501
+ NSArray<NSString *> *effectiveChangedProps = changedProps;
502
+ BOOL pathActuallyChanged = NO;
392
503
 
504
+ if ([changedProps containsObject:@"path"]) {
505
+ // CRITICAL FIX: Only reset state if the path actually changed
506
+ // React Native sometimes includes path in changedProps even when only page changes
507
+
508
+ if (_pdfDocument != Nil && _pdfDocument.documentURL != nil) {
509
+ // Compare new path with existing document's path
510
+ NSString *currentPath = _pdfDocument.documentURL.path;
511
+ NSString *newPath = _path;
512
+ // Normalize paths for comparison (remove trailing slashes, resolve symlinks, etc.)
513
+ if (![currentPath isEqualToString:newPath]) {
514
+ pathActuallyChanged = YES;
515
+ }
516
+ } else {
517
+ // No existing document, so this is a new path (or initial load)
518
+ pathActuallyChanged = YES;
519
+ }
520
+
521
+ RCTLogInfo(@"🔄 [iOS Scroll] Path prop in changedProps - hadDocument=%d, pathActuallyChanged=%d",
522
+ (_pdfDocument != Nil), pathActuallyChanged);
523
+
524
+ // Filter out "path" from effectiveChangedProps if it hasn't actually changed
525
+ if (!pathActuallyChanged) {
526
+ RCTLogInfo(@"⏭️ [iOS Scroll] Path value unchanged, filtering out 'path' from effectiveChangedProps");
527
+ NSMutableArray<NSString *> *filtered = [changedProps mutableCopy];
528
+ [filtered removeObject:@"path"];
529
+ effectiveChangedProps = filtered;
530
+ } else {
531
+ // Path actually changed, use changedProps as-is
532
+ effectiveChangedProps = changedProps;
533
+ }
534
+
535
+ if (!pathActuallyChanged) {
536
+ RCTLogInfo(@"⏭️ [iOS Scroll] Path value unchanged, skipping document reload");
537
+ // Skip the rest of path handling
538
+ } else {
539
+ // Reset document load state when path actually changes
540
+ _documentLoaded = NO;
541
+ _previousPage = -1;
542
+ _isNavigating = NO;
393
543
 
544
+ // Release old doc if it exists
394
545
  if (_pdfDocument != Nil) {
395
- //Release old doc
396
546
  _pdfDocument = Nil;
547
+ _usePageViewControllerStateInitialized = NO;
548
+ _currentUsePageViewController = NO;
549
+ RCTLogInfo(@"🔄 [iOS Scroll] Reset usePageViewController state - path changed (hadDocument=YES)");
550
+ } else {
551
+ RCTLogInfo(@"⏭️ [iOS Scroll] No previous document to reset");
397
552
  }
398
553
 
399
554
  if ([_path hasPrefix:@"blob:"]) {
@@ -429,16 +584,31 @@ using namespace facebook::react;
429
584
  }
430
585
 
431
586
  _pdfView.document = _pdfDocument;
587
+ _documentLoaded = YES;
588
+
589
+ // Configure scroll view after document is set
590
+ // PDFKit creates the scroll view lazily, so we need to wait a bit
591
+ dispatch_async(dispatch_get_main_queue(), ^{
592
+ RCTLogInfo(@"🔍 [iOS Scroll] Document set, searching for scroll view");
593
+ [self configureScrollView:self->_pdfView enabled:self->_scrollEnabled depth:0];
594
+
595
+ // Retry after a short delay to catch cases where scroll view is created asynchronously
596
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
597
+ RCTLogInfo(@"🔍 [iOS Scroll] Retry search for scroll view after delay");
598
+ [self configureScrollView:self->_pdfView enabled:self->_scrollEnabled depth:0];
599
+ });
600
+ });
432
601
  } else {
433
602
 
434
603
  [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"error|Load pdf failed. path=%s",_path.UTF8String]]];
435
604
 
436
605
  _pdfDocument = Nil;
437
606
  return;
607
+ }
438
608
  }
439
609
  }
440
610
 
441
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"spacing"])) {
611
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"spacing"])) {
442
612
  if (_horizontal) {
443
613
  _pdfView.pageBreakMargins = UIEdgeInsetsMake(0,_spacing,0,0);
444
614
  if (_spacing==0) {
@@ -464,11 +634,11 @@ using namespace facebook::react;
464
634
  }
465
635
  }
466
636
 
467
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"enableRTL"])) {
637
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"enableRTL"])) {
468
638
  _pdfView.displaysRTL = _enableRTL;
469
639
  }
470
640
 
471
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"enableAnnotationRendering"])) {
641
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"enableAnnotationRendering"])) {
472
642
  if (!_enableAnnotationRendering) {
473
643
  for (unsigned long i=0; i<_pdfView.document.pageCount; i++) {
474
644
  PDFPage *pdfPage = [_pdfView.document pageAtIndex:i];
@@ -479,7 +649,7 @@ using namespace facebook::react;
479
649
  }
480
650
  }
481
651
 
482
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"fitPolicy"] || [changedProps containsObject:@"minScale"] || [changedProps containsObject:@"maxScale"])) {
652
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"fitPolicy"] || [changedProps containsObject:@"minScale"] || [changedProps containsObject:@"maxScale"])) {
483
653
 
484
654
  PDFPage *pdfPage = _pdfView.currentPage ? _pdfView.currentPage : [_pdfDocument pageAtIndex:_pdfDocument.pageCount-1];
485
655
  CGRect pdfPageRect = [pdfPage boundsForBox:kPDFDisplayBoxCropBox];
@@ -517,85 +687,282 @@ using namespace facebook::react;
517
687
 
518
688
  }
519
689
 
520
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"scale"])) {
690
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"scale"])) {
521
691
  _pdfView.scaleFactor = _scale * _fixScaleFactor;
522
692
  if (_pdfView.scaleFactor>_pdfView.maxScaleFactor) _pdfView.scaleFactor = _pdfView.maxScaleFactor;
523
693
  if (_pdfView.scaleFactor<_pdfView.minScaleFactor) _pdfView.scaleFactor = _pdfView.minScaleFactor;
524
694
  }
525
695
 
526
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"horizontal"])) {
696
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"horizontal"])) {
527
697
  if (_horizontal) {
528
698
  _pdfView.displayDirection = kPDFDisplayDirectionHorizontal;
529
699
  _pdfView.pageBreakMargins = UIEdgeInsetsMake(0,_spacing,0,0);
700
+ RCTLogInfo(@"➡️ [iOS Scroll] Set display direction to HORIZONTAL (spacing=%d)", _spacing);
530
701
  } else {
531
702
  _pdfView.displayDirection = kPDFDisplayDirectionVertical;
532
703
  _pdfView.pageBreakMargins = UIEdgeInsetsMake(0,0,_spacing,0);
704
+ RCTLogInfo(@"⬇️ [iOS Scroll] Set display direction to VERTICAL (spacing=%d)", _spacing);
533
705
  }
534
706
  }
535
707
 
536
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"enablePaging"])) {
537
- if (_enablePaging) {
708
+ // CRITICAL FIX: Only configure usePageViewController when path changes (document loading)
709
+ // This prevents unnecessary reconfigurations during scrolling and layout updates
710
+ // Once configured, usePageViewController doesn't need to be reconfigured unless the document changes
711
+ if (_pdfDocument && [effectiveChangedProps containsObject:@"path"]) {
712
+ // Fix: Disable usePageViewController when horizontal is true, as it conflicts with horizontal scrolling
713
+ // UIPageViewController doesn't work well with horizontal PDFView display direction
714
+ BOOL shouldUsePageViewController = _enablePaging && !_horizontal;
715
+
716
+ RCTLogInfo(@"🔄 [iOS Scroll] Configuring usePageViewController on document load - enablePaging=%d, horizontal=%d, usePageVC=%d",
717
+ _enablePaging, _horizontal, shouldUsePageViewController);
718
+
719
+ // Set state immediately
720
+ _currentUsePageViewController = shouldUsePageViewController;
721
+ _usePageViewControllerStateInitialized = YES;
722
+
723
+ // Configure usePageViewController - this only happens on document load
724
+ if (shouldUsePageViewController) {
725
+ // Only use page view controller for vertical orientation
538
726
  [_pdfView usePageViewController:YES withViewOptions:@{UIPageViewControllerOptionSpineLocationKey:@(UIPageViewControllerSpineLocationMin),UIPageViewControllerOptionInterPageSpacingKey:@(_spacing)}];
727
+ RCTLogInfo(@"✅ [iOS Scroll] Enabled UIPageViewController (vertical paging mode)");
539
728
  } else {
729
+ // For horizontal or when paging is disabled, use regular scrolling
540
730
  [_pdfView usePageViewController:NO withViewOptions:Nil];
731
+ RCTLogInfo(@"✅ [iOS Scroll] Disabled UIPageViewController (using regular scrolling)");
541
732
  }
733
+
734
+ // Reconfigure scroll view after usePageViewController changes
735
+ // PDFView's internal scroll view hierarchy changes when usePageViewController is toggled
736
+ dispatch_async(dispatch_get_main_queue(), ^{
737
+ // Reset scroll view references to allow reconfiguration
738
+ RCTLogInfo(@"🔄 [iOS Scroll] Resetting scroll view references for reconfiguration");
739
+ self->_internalScrollView = nil;
740
+ self->_originalScrollDelegate = nil;
741
+ self->_scrollDelegateProxy = nil;
742
+
743
+ // Reconfigure scroll view after view hierarchy updates
744
+ dispatch_async(dispatch_get_main_queue(), ^{
745
+ RCTLogInfo(@"🔧 [iOS Scroll] Reconfiguring scroll view after usePageViewController change (scrollEnabled=%d)", self->_scrollEnabled);
746
+ [self configureScrollView:self->_pdfView enabled:self->_scrollEnabled depth:0];
747
+ });
748
+ });
542
749
  }
543
750
 
544
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"singlePage"])) {
751
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"singlePage"])) {
545
752
  if (_singlePage) {
546
753
  _pdfView.displayMode = kPDFDisplaySinglePage;
547
754
  _pdfView.userInteractionEnabled = NO;
755
+ RCTLogInfo(@"📄 [iOS Scroll] Set to SINGLE PAGE mode (userInteractionEnabled=NO)");
548
756
  } else {
549
757
  _pdfView.displayMode = kPDFDisplaySinglePageContinuous;
550
758
  _pdfView.userInteractionEnabled = YES;
759
+ RCTLogInfo(@"📄 [iOS Scroll] Set to CONTINUOUS PAGE mode (userInteractionEnabled=YES)");
551
760
  }
552
761
  }
553
762
 
554
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"showsHorizontalScrollIndicator"] || [changedProps containsObject:@"showsVerticalScrollIndicator"])) {
763
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] || [changedProps containsObject:@"showsHorizontalScrollIndicator"] || [changedProps containsObject:@"showsVerticalScrollIndicator"])) {
555
764
  [self setScrollIndicators:self horizontal:_showsHorizontalScrollIndicator vertical:_showsVerticalScrollIndicator depth:0];
556
765
  }
557
766
 
558
767
  // Configure scroll view (scrollEnabled)
559
- if (_pdfDocument && ([changedProps containsObject:@"path"] ||
768
+ if (_pdfDocument && ([effectiveChangedProps containsObject:@"path"] ||
560
769
  [changedProps containsObject:@"scrollEnabled"])) {
770
+ RCTLogInfo(@"🔧 [iOS Scroll] Configuring scroll enabled=%d (path changed=%d, scrollEnabled changed=%d)",
771
+ _scrollEnabled,
772
+ [effectiveChangedProps containsObject:@"path"],
773
+ [changedProps containsObject:@"scrollEnabled"]);
774
+
561
775
  // If path changed, restore original delegate before reconfiguring
562
- if ([changedProps containsObject:@"path"] && _internalScrollView && _originalScrollDelegate) {
563
- _internalScrollView.delegate = _originalScrollDelegate;
776
+ if ([effectiveChangedProps containsObject:@"path"] && _internalScrollView) {
777
+ RCTLogInfo(@"🔄 [iOS Scroll] Restoring original scroll delegate (path changed)");
778
+ if (_originalScrollDelegate) {
779
+ _internalScrollView.delegate = _originalScrollDelegate;
780
+ } else {
781
+ _internalScrollView.delegate = nil;
782
+ }
564
783
  _internalScrollView = nil;
565
784
  _originalScrollDelegate = nil;
785
+ _scrollDelegateProxy = nil;
566
786
  }
567
787
 
568
788
  // Use dispatch_async to ensure view hierarchy is fully set up after document load
569
789
  dispatch_async(dispatch_get_main_queue(), ^{
570
790
  // Search within _pdfView's hierarchy for scroll views
791
+ RCTLogInfo(@"🔍 [iOS Scroll] Starting scroll view search in PDFView hierarchy");
571
792
  [self configureScrollView:self->_pdfView enabled:self->_scrollEnabled depth:0];
572
793
  });
573
794
  }
574
795
 
575
- if (_pdfDocument && ([changedProps containsObject:@"path"] || [changedProps containsObject:@"enablePaging"] || [changedProps containsObject:@"horizontal"] || [changedProps containsObject:@"page"])) {
576
-
796
+ // Separate page navigation logic - only navigate when page prop actually changes
797
+ // Skip navigation on initial load (when path changes) to avoid conflicts
798
+ BOOL shouldNavigateToPage = _documentLoaded &&
799
+ [changedProps containsObject:@"page"] &&
800
+ !_isNavigating &&
801
+ _page != _previousPage &&
802
+ _page > 0 &&
803
+ _page <= (int)_pdfDocument.pageCount;
804
+
805
+ // #region agent log
806
+ if ([changedProps containsObject:@"page"]) {
807
+ NSString *logPath14 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
808
+ NSDictionary *logEntry14 = @{
809
+ @"sessionId": @"debug-session",
810
+ @"runId": @"init",
811
+ @"hypothesisId": @"A,C,D",
812
+ @"location": @"RNPDFPdfView.mm:803",
813
+ @"message": @"updateProps: page prop changed - checking shouldNavigateToPage",
814
+ @"data": @{
815
+ @"_page": @(_page),
816
+ @"_previousPage": @(_previousPage),
817
+ @"documentLoaded": @(_documentLoaded),
818
+ @"isNavigating": @(_isNavigating),
819
+ @"shouldNavigateToPage": @(shouldNavigateToPage),
820
+ @"contentOffsetBeforeNav": _internalScrollView ? @{@"x": @(_internalScrollView.contentOffset.x), @"y": @(_internalScrollView.contentOffset.y)} : @"noScrollView"
821
+ },
822
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
823
+ };
824
+ NSData *logData14 = [NSJSONSerialization dataWithJSONObject:logEntry14 options:0 error:nil];
825
+ NSString *logLine14 = [[NSString alloc] initWithData:logData14 encoding:NSUTF8StringEncoding];
826
+ NSFileHandle *fileHandle14 = [NSFileHandle fileHandleForWritingAtPath:logPath14];
827
+ if (fileHandle14) {
828
+ [fileHandle14 seekToEndOfFile];
829
+ [fileHandle14 writeData:[[logLine14 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
830
+ [fileHandle14 closeFile];
831
+ } else {
832
+ [[logLine14 stringByAppendingString:@"\n"] writeToFile:logPath14 atomically:YES encoding:NSUTF8StringEncoding error:nil];
833
+ }
834
+ }
835
+ // #endregion
836
+
837
+ if (shouldNavigateToPage) {
838
+ _isNavigating = YES;
839
+ PDFPage *pdfPage = [_pdfDocument pageAtIndex:_page-1];
840
+
841
+ if (pdfPage) {
842
+ // Use smooth navigation instead of instant jump to prevent full rerender
843
+ dispatch_async(dispatch_get_main_queue(), ^{
844
+ // #region agent log
845
+ CGPoint contentOffsetBefore = self->_internalScrollView ? self->_internalScrollView.contentOffset : CGPointMake(0, 0);
846
+ NSString *logPath15 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
847
+ NSDictionary *logEntry15 = @{
848
+ @"sessionId": @"debug-session",
849
+ @"runId": @"init",
850
+ @"hypothesisId": @"B,C",
851
+ @"location": @"RNPDFPdfView.mm:812",
852
+ @"message": @"goToDestination: BEFORE navigation call",
853
+ @"data": @{
854
+ @"targetPage": @(self->_page),
855
+ @"enablePaging": @(self->_enablePaging),
856
+ @"contentOffsetBefore": @{@"x": @(contentOffsetBefore.x), @"y": @(contentOffsetBefore.y)},
857
+ @"isNavigating": @(self->_isNavigating)
858
+ },
859
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
860
+ };
861
+ NSData *logData15 = [NSJSONSerialization dataWithJSONObject:logEntry15 options:0 error:nil];
862
+ NSString *logLine15 = [[NSString alloc] initWithData:logData15 encoding:NSUTF8StringEncoding];
863
+ NSFileHandle *fileHandle15 = [NSFileHandle fileHandleForWritingAtPath:logPath15];
864
+ if (fileHandle15) {
865
+ [fileHandle15 seekToEndOfFile];
866
+ [fileHandle15 writeData:[[logLine15 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
867
+ [fileHandle15 closeFile];
868
+ } else {
869
+ [[logLine15 stringByAppendingString:@"\n"] writeToFile:logPath15 atomically:YES encoding:NSUTF8StringEncoding error:nil];
870
+ }
871
+ // #endregion
872
+
873
+ if (!self->_enablePaging) {
874
+ // For non-paging mode, use animated navigation
875
+ CGRect pdfPageRect = [pdfPage boundsForBox:kPDFDisplayBoxCropBox];
876
+
877
+ // Handle page rotation
878
+ if (pdfPage.rotation == 90 || pdfPage.rotation == 270) {
879
+ pdfPageRect = CGRectMake(0, 0, pdfPageRect.size.height, pdfPageRect.size.width);
880
+ }
881
+
882
+ CGPoint pointLeftTop = CGPointMake(0, pdfPageRect.size.height);
883
+ PDFDestination *pdfDest = [[PDFDestination alloc] initWithPage:pdfPage atPoint:pointLeftTop];
884
+
885
+ // Use goToDestination for smooth navigation
886
+ [self->_pdfView goToDestination:pdfDest];
887
+ self->_pdfView.scaleFactor = self->_fixScaleFactor * self->_scale;
888
+ } else {
889
+ // For paging mode, use goToRect for better page alignment
890
+ if (self->_page == 1) {
891
+ // Special case for first page
892
+ [self->_pdfView goToRect:CGRectMake(0, NSUIntegerMax, 1, 1) onPage:pdfPage];
893
+ } else {
894
+ CGRect pdfPageRect = [pdfPage boundsForBox:kPDFDisplayBoxCropBox];
895
+ if (pdfPage.rotation == 90 || pdfPage.rotation == 270) {
896
+ pdfPageRect = CGRectMake(0, 0, pdfPageRect.size.height, pdfPageRect.size.width);
897
+ }
898
+ CGPoint pointLeftTop = CGPointMake(0, pdfPageRect.size.height);
899
+ PDFDestination *pdfDest = [[PDFDestination alloc] initWithPage:pdfPage atPoint:pointLeftTop];
900
+ [self->_pdfView goToDestination:pdfDest];
901
+ self->_pdfView.scaleFactor = self->_fixScaleFactor * self->_scale;
902
+ }
903
+ }
904
+
905
+ self->_previousPage = self->_page;
906
+ self->_isNavigating = NO;
907
+
908
+ // #region agent log
909
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
910
+ CGPoint contentOffsetAfter = self->_internalScrollView ? self->_internalScrollView.contentOffset : CGPointMake(0, 0);
911
+ NSString *logPath16 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
912
+ NSDictionary *logEntry16 = @{
913
+ @"sessionId": @"debug-session",
914
+ @"runId": @"init",
915
+ @"hypothesisId": @"B,C",
916
+ @"location": @"RNPDFPdfView.mm:845",
917
+ @"message": @"goToDestination: AFTER navigation call (100ms delay)",
918
+ @"data": @{
919
+ @"targetPage": @(self->_page),
920
+ @"previousPage": @(self->_previousPage),
921
+ @"contentOffsetBefore": @{@"x": @(contentOffsetBefore.x), @"y": @(contentOffsetBefore.y)},
922
+ @"contentOffsetAfter": @{@"x": @(contentOffsetAfter.x), @"y": @(contentOffsetAfter.y)},
923
+ @"offsetChanged": @(fabs(contentOffsetBefore.x - contentOffsetAfter.x) > 1 || fabs(contentOffsetBefore.y - contentOffsetAfter.y) > 1)
924
+ },
925
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
926
+ };
927
+ NSData *logData16 = [NSJSONSerialization dataWithJSONObject:logEntry16 options:0 error:nil];
928
+ NSString *logLine16 = [[NSString alloc] initWithData:logData16 encoding:NSUTF8StringEncoding];
929
+ NSFileHandle *fileHandle16 = [NSFileHandle fileHandleForWritingAtPath:logPath16];
930
+ if (fileHandle16) {
931
+ [fileHandle16 seekToEndOfFile];
932
+ [fileHandle16 writeData:[[logLine16 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
933
+ [fileHandle16 closeFile];
934
+ } else {
935
+ [[logLine16 stringByAppendingString:@"\n"] writeToFile:logPath16 atomically:YES encoding:NSUTF8StringEncoding error:nil];
936
+ }
937
+ });
938
+ // #endregion
939
+ });
940
+ } else {
941
+ _isNavigating = NO;
942
+ }
943
+ }
944
+
945
+ // Handle initial page on document load (only when path changes)
946
+ // This handles the case where the document was just loaded and we need to navigate to the initial page
947
+ // Use pathActuallyChanged instead of checking changedProps to ensure we only handle initial page when path actually changed
948
+ if (_pdfDocument && pathActuallyChanged && _documentLoaded) {
577
949
  PDFPage *pdfPage = [_pdfDocument pageAtIndex:_page-1];
578
950
  if (pdfPage && _page == 1) {
579
- // goToDestination() would be better. However, there is an
580
- // error in the pointLeftTop computation that often results in
581
- // scrolling to the middle of the page.
582
- // Special case workaround to make starting at the first page
583
- // align acceptably.
951
+ // Special case workaround for first page alignment
584
952
  dispatch_async(dispatch_get_main_queue(), ^{
585
953
  [self->_pdfView goToRect:CGRectMake(0, NSUIntegerMax, 1, 1) onPage:pdfPage];
954
+ self->_previousPage = self->_page;
586
955
  });
587
956
  } else if (pdfPage) {
588
957
  CGRect pdfPageRect = [pdfPage boundsForBox:kPDFDisplayBoxCropBox];
589
-
590
- // some pdf with rotation, then adjust it
591
958
  if (pdfPage.rotation == 90 || pdfPage.rotation == 270) {
592
959
  pdfPageRect = CGRectMake(0, 0, pdfPageRect.size.height, pdfPageRect.size.width);
593
960
  }
594
-
595
961
  CGPoint pointLeftTop = CGPointMake(0, pdfPageRect.size.height);
596
962
  PDFDestination *pdfDest = [[PDFDestination alloc] initWithPage:pdfPage atPoint:pointLeftTop];
597
963
  [_pdfView goToDestination:pdfDest];
598
964
  _pdfView.scaleFactor = _fixScaleFactor*_scale;
965
+ _previousPage = _page;
599
966
  }
600
967
  }
601
968
 
@@ -618,6 +985,15 @@ using namespace facebook::react;
618
985
  _initialed = YES;
619
986
 
620
987
  [self didSetProps:mProps];
988
+
989
+ // Configure scroll view after layout to ensure it's found
990
+ // This is important because PDFKit creates the scroll view lazily
991
+ if (_documentLoaded && _pdfDocument) {
992
+ dispatch_async(dispatch_get_main_queue(), ^{
993
+ RCTLogInfo(@"🔍 [iOS Scroll] reactSetFrame called, configuring scroll view after layout");
994
+ [self configureScrollView:self->_pdfView enabled:self->_scrollEnabled depth:0];
995
+ });
996
+ }
621
997
  }
622
998
 
623
999
 
@@ -791,7 +1167,21 @@ using namespace facebook::react;
791
1167
  unsigned long numberOfPages = _pdfDocument.pageCount;
792
1168
 
793
1169
  // Update current page for preloading
794
- _page = (int)page + 1;
1170
+ int newPage = (int)page + 1;
1171
+
1172
+ // CRITICAL FIX: Update _previousPage to the new page value when page changes from PDFView notifications
1173
+ // This prevents updateProps from triggering programmatic navigation when React Native
1174
+ // receives the pageChanged notification and updates the page prop back to us.
1175
+ // By setting _previousPage = newPage, when updateProps checks _page != _previousPage,
1176
+ // they will be equal (since the page prop will match the new page), and navigation will be skipped.
1177
+ if (newPage != _page) {
1178
+ _previousPage = newPage; // Set to newPage to prevent navigation loop
1179
+ _page = newPage;
1180
+ } else {
1181
+ // If page didn't actually change, just ensure _previousPage matches to prevent navigation
1182
+ _previousPage = _page;
1183
+ }
1184
+
795
1185
  _pageCount = (int)numberOfPages;
796
1186
  if (_enablePreloading) {
797
1187
  [self preloadAdjacentPages:_page];
@@ -1006,48 +1396,253 @@ using namespace facebook::react;
1006
1396
  }
1007
1397
 
1008
1398
  - (void)configureScrollView:(UIView *)view enabled:(BOOL)enabled depth:(int)depth {
1399
+ // Log entry to track all calls
1400
+ if (depth == 0) {
1401
+ RCTLogInfo(@"🚀 [iOS Scroll] configureScrollView called - enabled=%d, view=%@", enabled, NSStringFromClass([view class]));
1402
+ }
1403
+
1009
1404
  // max depth, prevent infinite loop
1010
1405
  if (depth > 10) {
1406
+ RCTLogWarn(@"⚠️ [iOS Scroll] Max depth reached in configureScrollView (depth=%d)", depth);
1011
1407
  return;
1012
1408
  }
1013
1409
 
1014
1410
  if ([view isKindOfClass:[UIScrollView class]]) {
1015
1411
  UIScrollView *scrollView = (UIScrollView *)view;
1412
+ RCTLogInfo(@"📱 [iOS Scroll] Found UIScrollView at depth=%d, frame=%@, contentSize=%@, enabled=%d",
1413
+ depth,
1414
+ NSStringFromCGRect(scrollView.frame),
1415
+ NSStringFromCGSize(scrollView.contentSize),
1416
+ enabled);
1417
+
1418
+ // #region agent log
1419
+ {
1420
+ NSString *logPath1 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
1421
+ NSDictionary *logEntry1 = @{
1422
+ @"sessionId": @"debug-session",
1423
+ @"runId": @"init",
1424
+ @"hypothesisId": @"D,F",
1425
+ @"location": @"RNPDFPdfView.mm:1307",
1426
+ @"message": @"Found UIScrollView in hierarchy",
1427
+ @"data": @{
1428
+ @"depth": @(depth),
1429
+ @"contentSize": @{@"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height)},
1430
+ @"frame": @{@"x": @(scrollView.frame.origin.x), @"y": @(scrollView.frame.origin.y), @"width": @(scrollView.frame.size.width), @"height": @(scrollView.frame.size.height)},
1431
+ @"scrollEnabled": @(scrollView.scrollEnabled),
1432
+ @"alwaysBounceHorizontal": @(scrollView.alwaysBounceHorizontal),
1433
+ @"userInteractionEnabled": @(scrollView.userInteractionEnabled),
1434
+ @"horizontal": @(_horizontal),
1435
+ @"enablePaging": @(_enablePaging)
1436
+ },
1437
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
1438
+ };
1439
+ NSData *logData1 = [NSJSONSerialization dataWithJSONObject:logEntry1 options:0 error:nil];
1440
+ NSString *logLine1 = [[NSString alloc] initWithData:logData1 encoding:NSUTF8StringEncoding];
1441
+ NSFileHandle *fileHandle1 = [NSFileHandle fileHandleForWritingAtPath:logPath1];
1442
+ if (fileHandle1) {
1443
+ [fileHandle1 seekToEndOfFile];
1444
+ [fileHandle1 writeData:[[logLine1 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
1445
+ [fileHandle1 closeFile];
1446
+ } else {
1447
+ [[logLine1 stringByAppendingString:@"\n"] writeToFile:logPath1 atomically:YES encoding:NSUTF8StringEncoding error:nil];
1448
+ }
1449
+ }
1450
+ // #endregion
1451
+
1016
1452
  // Since we're starting the recursion from _pdfView, all scroll views found are within its hierarchy
1017
1453
  // Configure scroll properties
1454
+ BOOL previousScrollEnabled = scrollView.scrollEnabled;
1018
1455
  scrollView.scrollEnabled = enabled;
1019
1456
 
1020
- // Disable horizontal bouncing to prevent interference with navigation swipe-back
1457
+ if (previousScrollEnabled != enabled) {
1458
+ RCTLogInfo(@"🔄 [iOS Scroll] Changed scrollEnabled: %d -> %d", previousScrollEnabled, enabled);
1459
+ }
1460
+
1461
+ // Conditionally set horizontal bouncing based on scroll direction
1462
+ // Allow horizontal bounce when horizontal scrolling is enabled
1463
+ BOOL previousAlwaysBounceHorizontal = scrollView.alwaysBounceHorizontal;
1464
+ if (_horizontal) {
1465
+ scrollView.alwaysBounceHorizontal = YES;
1466
+ } else {
1467
+ // Disable horizontal bouncing for vertical scrolling to prevent interference with navigation swipe-back
1021
1468
  scrollView.alwaysBounceHorizontal = NO;
1469
+ }
1470
+
1471
+ if (previousAlwaysBounceHorizontal != scrollView.alwaysBounceHorizontal) {
1472
+ RCTLogInfo(@"🔄 [iOS Scroll] Changed alwaysBounceHorizontal: %d -> %d (horizontal=%d)",
1473
+ previousAlwaysBounceHorizontal,
1474
+ scrollView.alwaysBounceHorizontal,
1475
+ _horizontal);
1476
+ }
1477
+
1022
1478
  // Keep vertical bounce enabled for natural scrolling feel
1023
1479
  scrollView.bounces = YES;
1024
1480
 
1025
- // Set delegate for scroll tracking (only once to avoid conflicts)
1026
- // Store original delegate before replacing it to preserve PDFView's internal scrolling
1481
+ RCTLogInfo(@"📊 [iOS Scroll] ScrollView config - scrollEnabled=%d, alwaysBounceHorizontal=%d, bounces=%d, delegate=%@",
1482
+ scrollView.scrollEnabled,
1483
+ scrollView.alwaysBounceHorizontal,
1484
+ scrollView.bounces,
1485
+ scrollView.delegate != nil ? @"set" : @"nil");
1486
+
1487
+ // #region agent log
1488
+ {
1489
+ NSString *logPath3 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
1490
+ NSDictionary *logEntry3 = @{
1491
+ @"sessionId": @"debug-session",
1492
+ @"runId": @"init",
1493
+ @"hypothesisId": @"A,B,C,D,E",
1494
+ @"location": @"RNPDFPdfView.mm:1374",
1495
+ @"message": @"ScrollView configuration completed",
1496
+ @"data": @{
1497
+ @"scrollEnabled": @(scrollView.scrollEnabled),
1498
+ @"alwaysBounceHorizontal": @(scrollView.alwaysBounceHorizontal),
1499
+ @"bounces": @(scrollView.bounces),
1500
+ @"contentSize": @{@"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height)},
1501
+ @"userInteractionEnabled": @(scrollView.userInteractionEnabled),
1502
+ @"delegate": scrollView.delegate != nil ? @"set" : @"nil",
1503
+ @"horizontal": @(_horizontal),
1504
+ @"enablePaging": @(_enablePaging)
1505
+ },
1506
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
1507
+ };
1508
+ NSData *logData3 = [NSJSONSerialization dataWithJSONObject:logEntry3 options:0 error:nil];
1509
+ NSString *logLine3 = [[NSString alloc] initWithData:logData3 encoding:NSUTF8StringEncoding];
1510
+ NSFileHandle *fileHandle3 = [NSFileHandle fileHandleForWritingAtPath:logPath3];
1511
+ if (fileHandle3) {
1512
+ [fileHandle3 seekToEndOfFile];
1513
+ [fileHandle3 writeData:[[logLine3 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
1514
+ [fileHandle3 closeFile];
1515
+ } else {
1516
+ [[logLine3 stringByAppendingString:@"\n"] writeToFile:logPath3 atomically:YES encoding:NSUTF8StringEncoding error:nil];
1517
+ }
1518
+ }
1519
+ // #endregion
1520
+
1521
+ // IMPORTANT: PDFKit relies on the scrollView delegate for pinch-zoom (viewForZoomingInScrollView).
1522
+ // Install a proxy delegate that forwards to the original delegate, while still letting us observe scroll events.
1027
1523
  if (!_internalScrollView) {
1524
+ RCTLogInfo(@"✅ [iOS Scroll] Setting internal scroll view reference");
1028
1525
  _internalScrollView = scrollView;
1029
- // Store original delegate if it exists and is not us
1030
1526
  if (scrollView.delegate && scrollView.delegate != self) {
1031
1527
  _originalScrollDelegate = scrollView.delegate;
1528
+ RCTLogInfo(@"📝 [iOS Scroll] Stored original scroll delegate");
1032
1529
  }
1033
- scrollView.delegate = self;
1530
+ if (_originalScrollDelegate) {
1531
+ _scrollDelegateProxy = [[RNPDFScrollViewDelegateProxy alloc] initWithPrimary:_originalScrollDelegate secondary:(id<UIScrollViewDelegate>)self];
1532
+ scrollView.delegate = (id<UIScrollViewDelegate>)_scrollDelegateProxy;
1533
+ RCTLogInfo(@"🔗 [iOS Scroll] Installed scroll delegate proxy");
1534
+ } else {
1535
+ scrollView.delegate = self;
1536
+ RCTLogInfo(@"🔗 [iOS Scroll] Set self as scroll delegate");
1537
+ }
1538
+ } else {
1539
+ RCTLogInfo(@"⚠️ [iOS Scroll] Internal scroll view already set, skipping delegate setup");
1034
1540
  }
1035
1541
  }
1036
1542
 
1037
1543
  for (UIView *subview in view.subviews) {
1038
1544
  [self configureScrollView:subview enabled:enabled depth:depth + 1];
1039
1545
  }
1546
+
1547
+ // Log at root level if no scroll view was found
1548
+ if (depth == 0 && !_internalScrollView) {
1549
+ RCTLogWarn(@"⚠️ [iOS Scroll] No UIScrollView found in view hierarchy (view=%@, subviewCount=%lu)",
1550
+ NSStringFromClass([view class]),
1551
+ (unsigned long)[view.subviews count]);
1552
+
1553
+ // #region agent log
1554
+ {
1555
+ NSString *logPath6 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
1556
+ NSMutableArray *subviewClasses = [NSMutableArray array];
1557
+ for (UIView *subview in view.subviews) {
1558
+ [subviewClasses addObject:NSStringFromClass([subview class])];
1559
+ }
1560
+ NSDictionary *logEntry6 = @{
1561
+ @"sessionId": @"debug-session",
1562
+ @"runId": @"init",
1563
+ @"hypothesisId": @"F",
1564
+ @"location": @"RNPDFPdfView.mm:1446",
1565
+ @"message": @"No UIScrollView found in hierarchy",
1566
+ @"data": @{
1567
+ @"viewClass": NSStringFromClass([view class]),
1568
+ @"subviewCount": @([view.subviews count]),
1569
+ @"subviewClasses": subviewClasses,
1570
+ @"horizontal": @(_horizontal),
1571
+ @"enablePaging": @(_enablePaging)
1572
+ },
1573
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
1574
+ };
1575
+ NSData *logData6 = [NSJSONSerialization dataWithJSONObject:logEntry6 options:0 error:nil];
1576
+ NSString *logLine6 = [[NSString alloc] initWithData:logData6 encoding:NSUTF8StringEncoding];
1577
+ NSFileHandle *fileHandle6 = [NSFileHandle fileHandleForWritingAtPath:logPath6];
1578
+ if (fileHandle6) {
1579
+ [fileHandle6 seekToEndOfFile];
1580
+ [fileHandle6 writeData:[[logLine6 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
1581
+ [fileHandle6 closeFile];
1582
+ } else {
1583
+ [[logLine6 stringByAppendingString:@"\n"] writeToFile:logPath6 atomically:YES encoding:NSUTF8StringEncoding error:nil];
1584
+ }
1585
+ }
1586
+ // #endregion
1587
+ }
1040
1588
  }
1041
1589
 
1042
1590
  #pragma mark - UIScrollViewDelegate
1043
1591
 
1044
1592
  - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
1045
- // Forward to original delegate first if it exists (important for PDFView's scrolling)
1046
- if (_originalScrollDelegate && [_originalScrollDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
1047
- [_originalScrollDelegate scrollViewDidScroll:scrollView];
1048
- }
1593
+ static int scrollEventCount = 0;
1594
+ scrollEventCount++;
1049
1595
 
1596
+ // Log scroll events periodically (every 10th event to avoid spam)
1597
+ if (scrollEventCount % 10 == 0) {
1598
+ RCTLogInfo(@"📜 [iOS Scroll] scrollViewDidScroll #%d - offset=(%.2f, %.2f), contentSize=(%.2f, %.2f), bounds=(%.2f, %.2f), scrollEnabled=%d",
1599
+ scrollEventCount,
1600
+ scrollView.contentOffset.x,
1601
+ scrollView.contentOffset.y,
1602
+ scrollView.contentSize.width,
1603
+ scrollView.contentSize.height,
1604
+ scrollView.bounds.size.width,
1605
+ scrollView.bounds.size.height,
1606
+ scrollView.scrollEnabled);
1607
+
1608
+ // #region agent log
1609
+ {
1610
+ NSString *logPath2 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
1611
+ NSDictionary *logEntry2 = @{
1612
+ @"sessionId": @"debug-session",
1613
+ @"runId": @"init",
1614
+ @"hypothesisId": @"B",
1615
+ @"location": @"RNPDFPdfView.mm:1300",
1616
+ @"message": @"scrollViewDidScroll called",
1617
+ @"data": @{
1618
+ @"eventCount": @(scrollEventCount),
1619
+ @"contentOffset": @{@"x": @(scrollView.contentOffset.x), @"y": @(scrollView.contentOffset.y)},
1620
+ @"contentSize": @{@"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height)},
1621
+ @"bounds": @{@"width": @(scrollView.bounds.size.width), @"height": @(scrollView.bounds.size.height)},
1622
+ @"scrollEnabled": @(scrollView.scrollEnabled),
1623
+ @"alwaysBounceHorizontal": @(scrollView.alwaysBounceHorizontal)
1624
+ },
1625
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
1626
+ };
1627
+ NSData *logData2 = [NSJSONSerialization dataWithJSONObject:logEntry2 options:0 error:nil];
1628
+ NSString *logLine2 = [[NSString alloc] initWithData:logData2 encoding:NSUTF8StringEncoding];
1629
+ NSFileHandle *fileHandle2 = [NSFileHandle fileHandleForWritingAtPath:logPath2];
1630
+ if (fileHandle2) {
1631
+ [fileHandle2 seekToEndOfFile];
1632
+ [fileHandle2 writeData:[[logLine2 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
1633
+ [fileHandle2 closeFile];
1634
+ } else {
1635
+ [[logLine2 stringByAppendingString:@"\n"] writeToFile:logPath2 atomically:YES encoding:NSUTF8StringEncoding error:nil];
1636
+ }
1637
+ }
1638
+ // #endregion
1639
+ }
1640
+
1050
1641
  if (!_pdfDocument || _singlePage) {
1642
+ if (scrollEventCount % 10 == 0) {
1643
+ RCTLogInfo(@"⏭️ [iOS Scroll] Skipping scroll handling - pdfDocument=%d, singlePage=%d",
1644
+ _pdfDocument != nil, _singlePage);
1645
+ }
1051
1646
  return;
1052
1647
  }
1053
1648
 
@@ -1068,7 +1663,46 @@ using namespace facebook::react;
1068
1663
 
1069
1664
  // Only update if page actually changed and is valid
1070
1665
  if (newPage != _page && newPage > 0 && newPage <= (int)_pdfDocument.pageCount) {
1666
+ RCTLogInfo(@"📄 [iOS Scroll] Page changed: %d -> %d (from scroll position)", _page, newPage);
1667
+ // #region agent log
1668
+ {
1669
+ NSString *logPath12 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
1670
+ NSDictionary *logEntry12 = @{
1671
+ @"sessionId": @"debug-session",
1672
+ @"runId": @"init",
1673
+ @"hypothesisId": @"A,C,D",
1674
+ @"location": @"RNPDFPdfView.mm:1558",
1675
+ @"message": @"scrollViewDidScroll detected page change - BEFORE updating _page",
1676
+ @"data": @{
1677
+ @"oldPage": @(_page),
1678
+ @"newPage": @(newPage),
1679
+ @"previousPage": @(_previousPage),
1680
+ @"contentOffset": @{@"x": @(scrollView.contentOffset.x), @"y": @(scrollView.contentOffset.y)},
1681
+ @"isNavigating": @(_isNavigating)
1682
+ },
1683
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
1684
+ };
1685
+ NSData *logData12 = [NSJSONSerialization dataWithJSONObject:logEntry12 options:0 error:nil];
1686
+ NSString *logLine12 = [[NSString alloc] initWithData:logData12 encoding:NSUTF8StringEncoding];
1687
+ NSFileHandle *fileHandle12 = [NSFileHandle fileHandleForWritingAtPath:logPath12];
1688
+ if (fileHandle12) {
1689
+ [fileHandle12 seekToEndOfFile];
1690
+ [fileHandle12 writeData:[[logLine12 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
1691
+ [fileHandle12 closeFile];
1692
+ } else {
1693
+ [[logLine12 stringByAppendingString:@"\n"] writeToFile:logPath12 atomically:YES encoding:NSUTF8StringEncoding error:nil];
1694
+ }
1695
+ }
1696
+ // #endregion
1697
+
1698
+ // CRITICAL FIX: Update _previousPage to the new page value when page changes from user scrolling
1699
+ // This prevents updateProps from triggering programmatic navigation when React Native
1700
+ // receives the pageChanged notification and updates the page prop back to us.
1701
+ // By setting _previousPage = newPage, when updateProps checks _page != _previousPage,
1702
+ // they will be equal (since React Native will set _page = newPage), and navigation will be skipped.
1703
+ int oldPage = _page;
1071
1704
  _page = newPage;
1705
+ _previousPage = newPage; // Set to newPage to prevent navigation loop
1072
1706
  _pageCount = (int)_pdfDocument.pageCount;
1073
1707
 
1074
1708
  // Trigger preloading if enabled
@@ -1078,6 +1712,39 @@ using namespace facebook::react;
1078
1712
 
1079
1713
  // Notify about page change
1080
1714
  [self notifyOnChangeWithMessage:[[NSString alloc] initWithString:[NSString stringWithFormat:@"pageChanged|%d|%lu", newPage, _pdfDocument.pageCount]]];
1715
+ // #region agent log
1716
+ {
1717
+ NSString *logPath13 = @"/Users/punithmanthri/Documents/github jsi folder /react-native-enhanced-pdf/.cursor/debug.log";
1718
+ NSDictionary *logEntry13 = @{
1719
+ @"sessionId": @"debug-session",
1720
+ @"runId": @"init",
1721
+ @"hypothesisId": @"A,C,D",
1722
+ @"location": @"RNPDFPdfView.mm:1570",
1723
+ @"message": @"scrollViewDidScroll detected page change - AFTER updating _page and _previousPage (to prevent navigation loop)",
1724
+ @"data": @{
1725
+ @"_page": @(_page),
1726
+ @"_previousPage": @(_previousPage),
1727
+ @"notificationSent": @YES
1728
+ },
1729
+ @"timestamp": @((long long)([[NSDate date] timeIntervalSince1970] * 1000))
1730
+ };
1731
+ NSData *logData13 = [NSJSONSerialization dataWithJSONObject:logEntry13 options:0 error:nil];
1732
+ NSString *logLine13 = [[NSString alloc] initWithData:logData13 encoding:NSUTF8StringEncoding];
1733
+ NSFileHandle *fileHandle13 = [NSFileHandle fileHandleForWritingAtPath:logPath13];
1734
+ if (fileHandle13) {
1735
+ [fileHandle13 seekToEndOfFile];
1736
+ [fileHandle13 writeData:[[logLine13 stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]];
1737
+ [fileHandle13 closeFile];
1738
+ } else {
1739
+ [[logLine13 stringByAppendingString:@"\n"] writeToFile:logPath13 atomically:YES encoding:NSUTF8StringEncoding error:nil];
1740
+ }
1741
+ }
1742
+ // #endregion
1743
+ }
1744
+ } else {
1745
+ if (scrollEventCount % 50 == 0) {
1746
+ RCTLogWarn(@"⚠️ [iOS Scroll] No visible page found for scroll position (%.2f, %.2f)",
1747
+ pdfPoint.x, pdfPoint.y);
1081
1748
  }
1082
1749
  }
1083
1750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-pdf-jsi",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "summary": "High-performance React Native PDF viewer with JSI acceleration - up to 80x faster than traditional bridge",
5
5
  "description": "🚀 Ultra-fast React Native PDF viewer with JSI (JavaScript Interface) integration for maximum performance. Features lazy loading, smart caching, progressive loading, and zero-bridge overhead operations. Perfect for large PDF files with 30-day persistent cache and advanced memory optimization. Google Play 16KB page size compliant for Android 15+. Supports iOS, Android, and Windows platforms.",
6
6
  "main": "index.js",