Michael Behan
Michael Behan

Reputation: 3443

Detect that a PDFView did scroll

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.

Upvotes: 9

Views: 6197

Answers (5)

chengfu.xiong
chengfu.xiong

Reputation: 1

try this!

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

and observe the scrollview delegate

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

Upvotes: 0

Josh
Josh

Reputation: 1718

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
    }
}

Upvotes: 3

arlomedia
arlomedia

Reputation: 9061

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];
    }
}

Upvotes: 4

Matthijs
Matthijs

Reputation: 535

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
            }
        }
    }
} 

Upvotes: 3

Manikandan D
Manikandan D

Reputation: 1442

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")
}

Upvotes: 5

Related Questions