I want to allow the user to draw on an iOS 11 PDFKit document viewed in a PDFView. The drawing should ultimately be embedded inside the PDF.
The latter I have solved by adding a PDFAnnotation of type "ink" to the PDFPage with a UIBezierPath corresponding to the user's drawing.
However, how do I actually record the touches the user makes on top of the PDFView to create such an UIBezierPath?
I have tried overriding touchesBegan on the PDFView and on the PDFPage, but it is never called. I have tried adding a UIGestureRecognizer, but didn't accomplish anything.
I'm assuming that I need to afterwards use the PDFView instance method convert(_ point: CGPoint, to page: PDFPage) to convert the coordinates obtained to PDF coordinates suitable for the annotation.
In the end I solved the problem by creating a PDFViewController class extending UIViewController and UIGestureRecognizerDelegate. I added a PDFView as a subview, and a UIBarButtonItem to the navigationItem, that serves to toggle annotation mode.
I record the touches in a UIBezierPath called signingPath, and have the current annotation in currentAnnotation of type PDFAnnotation using the following code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { let position = touch.location(in: pdfView) signingPath = UIBezierPath() signingPath.move(to: pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!)) annotationAdded = false UIGraphicsBeginImageContext(CGSize(width: 800, height: 600)) lastPoint = pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!) } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { let position = touch.location(in: pdfView) let convertedPoint = pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!) let page = pdfView.page(for: position, nearest: true)! signingPath.addLine(to: convertedPoint) let rect = signingPath.bounds if( annotationAdded ) { pdfView.document?.page(at: 0)?.removeAnnotation(currentAnnotation) currentAnnotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil) var signingPathCentered = UIBezierPath() signingPathCentered.cgPath = signingPath.cgPath signingPathCentered.moveCenter(to: rect.center) currentAnnotation.add(signingPathCentered) pdfView.document?.page(at: 0)?.addAnnotation(currentAnnotation) } else { lastPoint = pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!) annotationAdded = true currentAnnotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil) currentAnnotation.add(signingPath) pdfView.document?.page(at: 0)?.addAnnotation(currentAnnotation) } } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first { let position = touch.location(in: pdfView) signingPath.addLine(to: pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!)) pdfView.document?.page(at: 0)?.removeAnnotation(currentAnnotation) let rect = signingPath.bounds let annotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil) annotation.color = UIColor(hex: 0x284283) signingPath.moveCenter(to: rect.center) annotation.add(signingPath) pdfView.document?.page(at: 0)?.addAnnotation(annotation) } }
The annotation toggle button just runs:
pdfView.isUserInteractionEnabled = !pdfView.isUserInteractionEnabled
This was really the key to it, as this disables scrolling on the PDF and enables me to receive the touch events.
The way the touch events are recorded and converted into PDFAnnotation immediately means that the annotation is visible while writing on the PDF, and that it is finally recorded into the correct position in the PDF - no matter the scroll position.
Making sure it ends up on the right page is just a matter of similarly changing the hardcoded 0 for page number to the pdfView.page(for: position, nearest:true) value.
I've done this by creating a new view class (eg Annotate View) and putting on top of the PDFView when the user is annotating.
This view uses it's default touchesBegan/touchesMoved/touchesEnded methods to create a bezier path following the gesture. Once the touch has ended, my view then saves it as an annotation on the pdf.
Note: you would need a way for the user to decide if they were in an annotating state.
For my main class
class MyViewController : UIViewController, PDFViewDelegate, VCDelegate { var pdfView: PDFView? var touchView: AnnotateView? override func loadView() { touchView = AnnotateView(frame: CGRect(x: 0, y: 0, width: 375, height: 600)) touchView?.backgroundColor = .clear touchView?.delegate = self view.addSubview(touchView!) } func addAnnotation(_ annotation: PDFAnnotation) { print("Anotation added") pdfView?.document?.page(at: 0)?.addAnnotation(annotation) } }
My annotation view
class AnnotateView: UIView { var path: UIBezierPath? var delegate: VCDelegate? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { // Initialize a new path for the user gesture path = UIBezierPath() path?.lineWidth = 4.0 var touch: UITouch = touches.first! path?.move(to: touch.location(in: self)) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { // Add new points to the path let touch: UITouch = touches.first! path?.addLine(to: touch.location(in: self)) self.setNeedsDisplay() } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { let touch = touches.first path?.addLine(to: touch!.location(in: self)) self.setNeedsDisplay() let annotation = PDFAnnotation(bounds: self.bounds, forType: .ink, withProperties: nil) annotation.add(self.path!) delegate?.addAnnotation(annotation) } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { self.touchesEnded(touches, with: event) } override func draw(_ rect: CGRect) { // Draw the path path?.stroke() } override init(frame: CGRect) { super.init(frame: frame) self.isMultipleTouchEnabled = false } }