9

Does PDFKit on iOS expose a PDFView's underlying UIScrollView or is there any other way to directly detect that the user has scrolled a PDFView?

My use case is to hide a nav bar when the document is scrolled so as a workaround I've added my own pan gesture recogniser to the PDFView's parent and I do the hiding in gestureRecognizerShouldBegin and always return false but I expect there's something more like UIScrollViewDelegate that I'm missing in the docs.

PaulMest
  • 12,925
  • 7
  • 53
  • 50
Michael Behan
  • 3,433
  • 2
  • 28
  • 38
  • A hacky solution: `PDFView` has a `PDFScrollView` subview (private class) which inherits from `UIScrollView`, so you can cast it and set `UIScrollViewDelegate` to it. I did a quick setup and it didn't have a delegate set, but you should check in case it might break things. But of course these kinds of solutions are unsafe because Apple might change how things work in the future. – Lësha Turkowski Jan 10 '18 at 10:23

5 Answers5

5

Try this,

NotificationCenter.default.addObserver(self, selector: #selector(handlePageChange(notification:)), name: Notification.Name.PDFViewPageChanged, object: nil)

@objc private func handlePageChange(notification: Notification)
{
    print("Page changed")
}
Manikandan D
  • 1,422
  • 1
  • 13
  • 25
  • 1
    regardless the circumstances, it'd be better to offer a more generic solution with checking _which_ scroll-view has sent the notification; we don't know how many scroll-views on the screen at the same time what may also post notifications. – holex Mar 14 '18 at 08:43
  • I tried to get scrollview delegate for pdfview scrolling but cant able to get. Above solution help me. If you can able to get delegate for PDFView please share that solution here. – Manikandan D Mar 14 '18 at 09:16
  • 1
    you don't need a delegate for that... the notification contains the information about the sender. – holex Mar 14 '18 at 09:43
  • setting the internal delegate to something else won't work because it will be set to nil afterwards. – Denis Loh Apr 11 '18 at 06:49
  • 5
    This solution doesn't answer the question, which is determining when the scroll view has changed, not when a new page as become dominant in the view. – jsbox Jun 28 '19 at 01:28
  • What jsbox said, this solution doesn't answer the question. – Matthijs Dec 17 '19 at 10:53
  • 1
    @Matthijs, Question - He need to hide navigation bar while scrolling PDFView, So I suggested this solution.If you looking for page number changes in PDFView then you use this in notification function ==> YourPDFView.currentPage. Thanks for your comment. – Manikandan D Dec 19 '19 at 06:45
  • Ok @ManikandanD – Matthijs Dec 20 '19 at 10:13
  • @ManikandanD, Can you please tell me how can i catch when annotation will be select. I have used PDFViewAnnotationWillHit notification but no luck. – Vikas Rajput Mar 16 '20 at 12:09
4

Does PDFKit on iOS expose a PDFView's underlying UIScrollView

No, but hopefully Apple will add this in the future. I remember that UIWebView didn't have it originally and it was added later.

or is there any other way to directly detect that the user has scrolled a PDFView

No, it looks like none of the notifications provided by PDFViewDelegate address this.

I'm migrating from UIWebView to PDFView and am using scrollViewDidScroll for a bunch of stuff, so I didn't want to rely on just adding a pan gesture recognizer. Building from @Matthijs's answer, I'm finding the UIScrollView inside the PDFView, making my class its delegate, then passing any events back to the scroll view (which was its own delegate before my class became the delegate) so it can respond to them, too. With UIWebView, this last step was not necessary, but with PDFView, zooming and possibly other functions won't work without it.

I'm overriding all the documented delegate methods to reduce the chance that this will break if Apple changes the internal function of PDFView. However, I had to check respondsToSelector in each method, because the original scroll view delegate doesn't currently implement all of them.

- (void)viewDidLoad {
    // create the PDFView and find its inner scrollView
    self.pdfView = [[PDFView alloc] init];
    for (UIView *subview in self.pdfView.subviews) {
        if ([subview isKindOfClass:[UIScrollView class]]) {
            self.scrollView = (UIScrollView *)subview;
        } else {
            for (UIView *subsubview in subview.subviews) {
                if ([subsubview isKindOfClass:[UIScrollView class]]) {
                    self.scrollView = (UIScrollView *)subsubview;
                }
            }
        }
    }
}

- (void)loadPDFDocument:(NSString *)URL {
    // load a document, then become the delegate for the scrollView (we have to do that after loading the document)
    PDFDocument *document = [[PDFDocument alloc] initWithURL:URL];
    self.pdfView.document = document;
    self.scrollView.delegate = self;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // *** respond to scroll events here ***

    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [scrollViewDelegate scrollViewDidScroll:scrollView];
    }
}

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
        [scrollViewDelegate scrollViewWillBeginDragging:scrollView];
    }
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) {
        [scrollViewDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
    }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
        [scrollViewDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
    }
}

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewShouldScrollToTop:)]) {
        return [scrollViewDelegate scrollViewShouldScrollToTop:scrollView];
    }
    return TRUE;
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidScrollToTop:)]) {
        [scrollViewDelegate scrollViewDidScrollToTop:scrollView];
    }
}

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillBeginDecelerating:)]) {
        [scrollViewDelegate scrollViewWillBeginDecelerating:scrollView];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) {
        [scrollViewDelegate scrollViewDidEndDecelerating:scrollView];
    }
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(viewForZoomingInScrollView:)]) {
        return [scrollViewDelegate viewForZoomingInScrollView:scrollView];
    }
    return nil;
}

- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillBeginZooming:withView:)]) {
        [scrollViewDelegate scrollViewWillBeginZooming:scrollView withView:view];
    }
}

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndZooming:withView:atScale:)]) {
        [scrollViewDelegate scrollViewDidEndZooming:scrollView withView:view atScale:scale];
    }
}

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidZoom:)]) {
        [scrollViewDelegate scrollViewDidZoom:scrollView];
    }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) {
        [scrollViewDelegate scrollViewDidEndScrollingAnimation:scrollView];
    }
}

- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView {
    UIScrollView <UIScrollViewDelegate> *scrollViewDelegate = (UIScrollView <UIScrollViewDelegate> *)self.scrollView;
    if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidChangeAdjustedContentInset:)]) {
        [scrollViewDelegate scrollViewDidChangeAdjustedContentInset:scrollView];
    }
}
arlomedia
  • 8,534
  • 5
  • 60
  • 108
3

I did this to detect zooming and panning on a pdfView to copy those gestures to a second pdfView, and it's working perfectly fine here. Got some help to detect vertical and horizontal panning by the PanDirectionGestureRecognizer I found here: stackoverflow.com/a/55635482/558112

class Document: UIViewController, UIScrollViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Subscribe to notifications.
        NotificationCenter.default.addObserver(self, selector: #selector(onPageZoomAndPan), name: .PDFViewScaleChanged, object: pdfView

        // get the scrollView in pdfView and attach gesture recognizers

        outerLoop: for subView in pdfView.subviews {
            for view in subView.subviews {
                if let scrollView = view as? UIScrollView {
                    let xScrollViewPanGesture = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(onPageZoomAndPan))
                    xScrollViewPanGesture.delegate = self
                    scrollView.addGestureRecognizer(xScrollViewPanGesture)

                    let yScrollViewPanGesture = PanDirectionGestureRecognizer(direction: .vertical, target: self, action: #selector(onPageZoomAndPan))
                    yScrollViewPanGesture.delegate = self
                    scrollView.addGestureRecognizer(yScrollViewPanGesture)

                    break outerLoop
                }
            }
        }
    }

    // MARK: - UIScrollViewDelegate

    @objc private func onPageZoomAndPan() {
        let rect = pdfView.convert(pdfView.bounds, to: pdfView.currentPage!)
        pdfViewSecondScreen.scaleFactor = pdfView.scaleFactor
        pdfViewSecondScreen.go(to: rect, on: pdfView.currentPage!)
    }
}



enum PanDirection {
    case vertical
    case horizontal
}


// UIKit.UIGestureRecognizerSubclass

import UIKit.UIGestureRecognizerSubclass

class PanDirectionGestureRecognizer: UIPanGestureRecognizer {

    let direction : PanDirection

    init(direction: PanDirection, target: AnyObject, action: Selector) {
        self.direction = direction
        super.init(target: target, action: action)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)

        if state == .began {
            let vel = velocity(in: self.view!)

            switch direction {
            case .horizontal where abs(vel.y) > abs(vel.x):
                state = .cancelled
            case .vertical where abs(vel.x) > abs(vel.y):
                state = .cancelled
            default:
                break
            }
        }
    }
} 
Matthijs
  • 515
  • 4
  • 8
  • When I tried this, the UIScrollView was at the first level of subviews, so I adapted this code to check each subview and then check the subviews of each subview if needed. – arlomedia Feb 17 '20 at 02:16
2

Decided on a solution other's haven't done yet. Went with key value observing on the contentOffset property of the underlying UIScrollView.

You can use this extension to run a callback every time the scroll offset changes.

var observation = pdfView.onScrollOffsetChanged { scroll in
    print("PDFView scrolled to \(scroll.contentOffset).")
}

The extension

extension PDFView {
    func onScrollOffsetChange(handler: @escaping (UIScrollView) -> Void) -> NSKeyValueObservation? {
        detectScrollView()?.observe(\.contentOffset) { scroll, _ in
            handler(scroll)
        }
    }
    
    private func detectScrollView() -> UIScrollView? {
        for view in subviews {
            if let scroll = view as? UIScrollView {
                return scroll
            } else {
                for subview in view.subviews {
                    if let scroll = subview as? UIScrollView {
                        return scroll
                    }
                }
            }
        }
        
        print("Unable to find a scrollView subview on a PDFView.")
        return nil
    }
}
Josh
  • 1,688
  • 4
  • 22
  • 35
0

try this!

(pdfView.subviews[0] as? UIScrollView)?.delegate = self

and observe the scrollview delegate

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.contentOffset.y > 0 {
        /// ...
    } else {
        /// ...
    }
}