iosswiftpdfkitpdfviewpdf-annotations

Restrict to move/drag the pdf annotation within PDF Page boundary


I am loading one PDF on PDF view using the PDF kit library. I added one custome view (same like PDF Annotation) on pdf view, and I am allowing users to move/drag that custom view on pdf view(within pdf view/container view) using UIPanGestureRecognizer. Here is a gif,

If you see this gif, there is one problem. That custom view is going outside of the pdf page. I want to restrict it. The custom view should move/drag within the pdf page only. How I can fix this? Is there a solution for it?

Here is the link sample project and all code - https://drive.google.com/file/d/1Ilhd8gp4AAxB_Q9G9swFbe4KQUHbpyGs/view?usp=sharing

Here is some code sample from project,

override func didMoveToSuperview() {
        addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan)))
    }

@objc func pan(_ gesture: UIPanGestureRecognizer) {
    translate(gesture.translation(in: self))
    gesture.setTranslation(.zero, in: self)
    setNeedsDisplay()
    print("Frames after moving : \(frame)")
}

and code used as an extension

extension  CGPoint {
    static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
        .init(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
    }
    static func +=(lhs: inout CGPoint, rhs: CGPoint) {
        lhs.x += rhs.x
        lhs.y += rhs.y
    }
}
extension UIView {
    func translate(_ translation: CGPoint) {
        let destination = center + translation
        let minX = frame.width/2
        let minY = frame.height/2
        let maxX = superview!.frame.width-minX
        let maxY = superview!.frame.height-minY
        center = CGPoint(
            x: min(maxX, max(minX, destination.x)),
            y: min(maxY ,max(minY, destination.y)))
    }
}

Code - Get PDF Page Height and Width

 let page = pdfDocument.page(at: 0)
 let pageRect = page?.bounds(for: .mediaBox)

 print("pdf page width =", (pageRect?.size.width)!, "pdf page height =", (pageRect?.size.height)!)

Solution

  • I would recommend PDFAnnotation rather that UIView for adding content onto the PDFView.

    It is not so easy to compare a UIView's frame within a PDFView due to their coordinate systems being different.

    Adding a PDFAnnotation to the PDFView works in sync with the PDF coordinate system whereas working with UIView, you will need to do some conversions between coordinate spaces and this can be tricky and not so accurate.

    PDFKit PDFView coordinate system add PDF Annotation UIView

    Here are some small changes I made to kind of get this to work with a view.

    First in your SignatoryXibView I added this function to show a red border when we are close to the edge

    func showWarning(_ show: Bool)
    {
        if show
        {
            contentView1.layer.borderColor = UIColor.red.cgColor
            return
        }
        
        contentView1.layer.borderColor = UIColor.green.cgColor
    }
    

    I believe the SignatoryXibView should not be responsible for detecting and preventing going out of its superviews bounds so I created a protocol which the ViewController needs to conform to so that it can prevent the SignatoryXibView going out of the PDFView bounds.

    protocol SignatoryViewDelegate: class
    {
        func signatoryView(_ signatoryView: SignatoryXibView,
                           didReceivePanGesture gesture: UIPanGestureRecognizer)
    }
    
    class SignatoryXibView: UIView {
    
        // Add this
        weak var delegate: SignatoryViewDelegate?
        
        // .. rest of your code
        
        @objc
        func pan(_ gesture: UIPanGestureRecognizer)
        {
            // Hand off view translation handling to the delegate
            self.delegate?.signatoryView(self,
                                         didReceivePanGesture: gesture)
        }
    }
    

    Now in your UIView extension, you created a translate method which works well however, you do not want to translate the view's location. You want to first check if the translation will go out of the desired boundary and prevent the translation from happening.

    extension UIView {
        
        // Your original code
        func translate(_ translation: CGPoint) {
            let destination = center + translation
            let minX = frame.width/2
            let minY = frame.height/2
            let maxX = superview!.frame.width-minX
            let maxY = superview!.frame.height-minY
            center = CGPoint(
                x: min(maxX, max(minX, destination.x)),
                y: min(maxY ,max(minY, destination.y)))
        }
        
        // I have added this function to return the new rect
        // that would happen if this view translated
        func translatedRect(_ translation: CGPoint) -> CGRect
        {
            // All of this is your calculation
            let destination = center + translation
            
            let minX = frame.width/2
            let minY = frame.height/2
            let maxX = superview!.frame.width-minX
            let maxY = superview!.frame.height-minY
            
            var rect = CGRect(origin: .zero,
                              size: CGSize(width: bounds.width,
                                           height: bounds.height))
            
            let midX = min(maxX, max(minX, destination.x))
            let midY = min(maxY ,max(minY, destination.y))
            
            // I am not translating here, just creating a new rect
            // of the view if it would be translated
            rect.origin = CGPoint(x: midX - frame.width/2,
                                  y: midY - frame.height/2)
            
            return rect
        }
    }
    

    In your ViewController's loadPDF() function, make your view controller the delegate of the SignatoryXibView

    // Add Sign on PdfView
    // Your code unchanged
    let customView = SignatoryXibView(frame: CGRect(x: 150, 
                                                    y: 140, 
                                                    width: 112, 
                                                    height: 58))
    customView.signatoryLabel.text = "John Doe"
    self.pdfView.addSubview(customView)
    
    //Add this
    customView.delegate = self
    

    Then finally you implement the delegate function we added earlier in the protocol to prevent the SignatoryXibView from going out the page's bounds.

    extension ViewController: SignatoryViewDelegate
    {
        func signatoryView(_ signatoryView: SignatoryXibView,
                           didReceivePanGesture gesture: UIPanGestureRecognizer)
        {
            // Get the location where the user has tapped
            let gestureTouchLocation = gesture.translation(in: signatoryView)
            
            // Get the new frame if the signature view would
            // be translated
            let updatedFrame
                = signatoryView.translatedRect(gestureTouchLocation)
            
            // Convert this rect from the Signature View's coordinate space
            // To the PDFView's coordinate space
            let updatedFrameConverted
            = pdfView.convert(updatedFrame,
                              to: pdfView.currentPage!)
            
            print("Updated frame: \(updatedFrame)")
            print("Updated frame converted: \(updatedFrameConverted)")
            print("Signature frame: \(signatoryView.frame)")
            print()
            
            // Retrieve the bounds of the current page
            // Handle the optional properly in your production app
            let pageRect = pdfView.currentPage!.bounds(for: .mediaBox)
            
            // Check if the new frame of SignatoryXibView is within the bounds of the pdf page
            if updatedFrameConverted.origin.x > CGFloat.zero &&
                updatedFrameConverted.origin.y + updatedFrameConverted.height < pageRect.height &&
                updatedFrameConverted.origin.x + updatedFrameConverted.width < pageRect.width &&
                updatedFrameConverted.origin.y > CGFloat.zero
            
            {
                // Since the view is within the bounds, you can update the views frame
                
                signatoryView.translate(gesture.translation(in: signatoryView))
                
                gesture.setTranslation(.zero, in: signatoryView)
                signatoryView.setNeedsDisplay()
                
                
                
                // Do not show any warning
                signatoryView.showWarning(false)
                
                return
            }
            
            // The view has reached the edge of the page so do not perform any view
            // translation and show the warning
            signatoryView.showWarning(true)
        }
    }
    

    The end result should give you the below result which prevents the view from going outside the page bounds and makes the view red to show it cannot go further:

    PDFKit PDFView prevent Annotation boundary UIView margins

    Final thoughts

    1. I recommend using PDFAnnotation when adding things to the PDF
    2. This will scale and scroll with the pages properly especially in scenarios like zooming a PDF where you might need to update your view again for it to be in the right location

    Update

    I updated some math in the func signatoryView function in the extension ViewController: SignatoryViewDelegate

    This should give you better results in terms of figuring out the right boundary and also it will work when you zoom:

    PDFView UIView boundary annotation page width page height

    However, since it is not added as an annotation, it will not scroll with the page but the same boundary will be observed on the next page.