iosswiftuiscrollviewuigesturerecognizerpdfview

How to resolve the conflict of gestures between PDFView and SCrollView [Swift5, iOS 14]


I am creating a PDF viewer for iPads, with which users can read a PDF by scrolling horizontally.

I created the following code to implement the single page view with page change with gestures (while consulting with How to create a single page vertical scrolling PDFView in Swift and elsewhere).

Although this approach works fine most of the time, I realized that gestures are not detected (or called) when a PDF file is zoomed in. Because of this, I cannot go to the next page by swiping the screen. Playing with the extension PDFView {} function I created, I found out that disabling the user interaction in subview enables me to detect the swipe gestures. However, now I cannot scroll the page inside the PDFView. I would appreciate it if you could help me figure out how to fix this.

What I would like to implement is something like ‎PDF Expert (https://apps.apple.com/us/app/pdf-expert-pdf-reader-editor/id743974925), where I can scroll over to the next page horizontally.

Thank very much you for your help in advance!

import UIKit
import PDFKit

//PDF Zoom scale
var scaleOfPdf: CGFloat = 4

extension PDFView {
    func disableBouncing(){
        for subview in subviews{
            if let scrollView = subview as? UIScrollView{
                scrollView.bounces = false
                return
            }
}

class ViewController: UIViewController, UIGestureRecognizerDelegate,  UIDocumentPickerDelegate {

    @IBOutlet weak var pdfView: PDFView!
    
    override func viewDidLoad(){
        super.viewDidLoad()


            pdfView.autoresizesSubviews = true
            pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleTopMargin, .flexibleLeftMargin]
            
            pdfView.displayDirection = .horizontal
            pdfView.displayMode = .singlePage
            pdfView.autoScales = true
            // setting a color for background
            pdfView.backgroundColor = .black
            pdfView.document = pdfDocument
            //            pdfView.usePageViewController(true, withViewOptions: [UIPageViewController.OptionsKey.interPageSpacing: 20])
            pdfView.maxScaleFactor = 4.0
            pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
            
            pdfView.disableBouncing()  


       //setting swipe gesture
        let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(respondLeftSwipeGesture(_:)))
        leftSwipeGesture.direction = [UISwipeGestureRecognizer.Direction.left]
        pdfView.addGestureRecognizer(leftSwipeGesture)
        
        let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(respondRightSwipeGesture(_:)))
        rightSwipeGesture.direction = [UISwipeGestureRecognizer.Direction.right]
        pdfView.addGestureRecognizer(rightSwipeGesture)

    }
    
        
    //setting swipe-gesture
    @objc func respondLeftSwipeGesture(_ sender: UISwipeGestureRecognizer) {
        print("left swipe was detected")
        if pdfView.document == nil { return }
        scaleOfPdf = pdfView.scaleFactor
        pdfView.goToNextPage(self)
        pdfView.scaleFactor = scaleOfPdf
    }
    
    @objc func respondRightSwipeGesture(_ sender: UISwipeGestureRecognizer) {
        print("right swipe was detected")
        if pdfView.document == nil { return }
        scaleOfPdf = pdfView.scaleFactor
        pdfView.goToPreviousPage(self)
        pdfView.scaleFactor = scaleOfPdf
    }
}

Solution

  • Gesture Recognizers are working as a chain or pipeline that processes touches - after one (G1) fails, second one (G2) tries to recognize its gesture. Here you have at least 4 recognizers - your 2 ones (left and right), and the 2 scrollView's ones (pan and pinch). I will give the brief solution that covers only scrollView's pan recognizer, if you'll see problems also with pinch - you'll need to follow the same approach.

    Let's say G1 is your left recognizer, and G2 is scrollView's pan recognizer. In order to make G2 process the same touches as G1, they should be told to recognize simultaneously.

    Also, the user might move his/her finger a bit horizontally while scrolling vertically, so in that case, you also want scrolling to start only after your G1 gives up on swipe and fails to recognize it.

    In order to achieve that, you should add this code to your VC.

    override func viewDidLoad(){
        super.viewDidLoad()
        
        ...
        
        leftSwipeGesture.delegate = self
        leftSwipeGesture.cancelsTouchesInView = false
        rightSwipeGesture.delegate = self
        rightSwipeGesture.cancelsTouchesInView = false
    }
    
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
                                    shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return gestureRecognizer == leftSwipeGesture
            || gestureRecognizer == rightSwipeGesture
            || otherGestureRecognizer == leftSwipeGesture
            || otherGestureRecognizer == rightSwipeGesture
    }
    
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
                                    shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let _ = gestureRecognizer as? UIPanGestureRecognizer else { return false }
        return otherGestureRecognizer == leftSwipeGesture
            || otherGestureRecognizer == rightSwipeGesture
    }
    
    optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
                                    shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        guard let _ = otherGestureRecognizer as? UIPanGestureRecognizer else { return false }
        return gestureRecognizer == leftSwipeGesture
            || gestureRecognizer == rightSwipeGesture
    }
    

    If UIGestureRecognizerDelegate methods that I added are not getting called, you'll need to create a subclass PDFView, make left/rightSwipeGesture.delegate = pdfView and override in your PDFView subclass its UIGestureRecognizerDelegate methods with this logic.