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)!)
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.
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:
Final thoughts
PDFAnnotation
when adding things to the PDFUpdate
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:
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.