在 iOS 11 个 PDFKit 文档上实施墨迹注释

Implement ink annotations on iOS 11 PDFKit document

我想允许用户在 PDFView 中查看的 iOS11 PDFKit 文档上绘图。绘图最终应嵌入到 PDF 中。

后者我通过向 PDFPage 添加 "ink" 类型的 PDFAnnotation 来解决,其中 UIBezierPath 对应于用户的绘图。

但是,我如何实际记录用户在 PDFView 上进行的触摸以创建这样的 UIBezierPath?

我已经尝试覆盖 PDFView 和 PDFPage 上的 touchesBegan,但它从未被调用过。我尝试添加 UIGestureRecognizer,但没有完成任何事情。

我假设我之后需要使用PDFView实例方法convert(_ point: CGPoint, to page: PDFPage) 将获取的坐标转换为适合注释的PDF坐标。

我通过创建一个新视图 class(例如注释视图)并在用户注释时将其置于 PDFView 之上来完成此操作。

此视图使用它的默认 touchesBegan/touchesMoved/touchesEnded 方法来创建跟随手势的贝塞尔曲线路径。触摸结束后,我的视图会将其保存为 pdf 上的注释。

注意:您需要一种方法让用户决定他们是否处于注释状态。

对于我的主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)
}
}

我的注释视图

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

最后我通过创建一个扩展 UIViewController 和 UIGestureRecognizerDelegate 的 PDFViewController class 解决了这个问题。我将 PDFView 添加为子视图,并将 UIBarButtonItem 添加到 navigationItem,用于切换注释模式。

我在名为 signingPath 的 UIBezierPath 中记录触摸,并使用以下代码在 PDFAnnotation 类型的 currentAnnotation 中添加当前注释:

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

注释切换按钮刚刚运行:

pdfView.isUserInteractionEnabled = !pdfView.isUserInteractionEnabled

这真的是它的关键,因为它会禁用 PDF 上的滚动并使我能够接收触摸事件。

记录触摸事件并将其转换为 PDFAnnotation 的方式立即意味着注释在 PDF 上书写时可见,并且最终记录到 PDF 中的正确位置 - 无论滚动位置如何。

确保它最终出现在正确的页面上只是将页码的硬编码 0 类似地更改为 pdfView.page(for: position, nearest:true) 值的问题。

编辑:

jksoegaard 的回答虽然启发了我在这个问题上的所有工作,但它有一个缺陷:在 touchedMoved 期间,创建了多个 PDF 注释,因此 PDF 页面变得充满注释,这会影响其加载时间严重。我编写了在 touchesMoved 阶段绘制 CAShapeLayer 的代码,并仅在 touchesEnded 阶段创建完整的 PDF 注释。

我的实现是 UIGestureRecognizer 的子类,它允许您在钢笔、荧光笔和橡皮擦之间进行选择,还可以选择颜色和宽度。它还包括一个撤消管理器。示例项目是 here.

原答案

除了 jksoegaard 的出色回答之外,还有一些对像我这样的新手的说明:

  1. 您需要在项目中包含 UIBezierPath+.swift 才能识别 .moveCenter 和 rect.center。从 https://github.com/xhamr/paintcode-path-scale 下载。

  2. 可以排除这些行:

     UIGraphicsBeginImageContext(CGSize(width: 800, height: 600))
    

    let page = pdfView.page(for: position, nearest: true)!
  1. 您需要在函数外声明一些全局变量:

     var signingPath = UIBezierPath()
     var annotationAdded : Bool?
     var lastPoint : CGPoint?
     var currentAnnotation : PDFAnnotation?
    
  2. 最后,如果你想让墨水更宽、颜色更漂亮,你需要做两件事:

一个。每次看到 annotation.add 或 currentAnnotation.add 之前,您需要(根据该函数的需要使用注释或 currentAnnotation):

    let b = PDFBorder()
    b.lineWidth = { choose a pixel number here }
    currentAnnotation?.border = b
    currentAnnotation?.color=UIColor.{ your color of choosing }

我建议为颜色指定一个低 alpha。结果很漂亮,并且受您的划水速度影响。例如,红色为:

    UIColor(red: 255/255.0, green: 0/255.0, blue: 0/255.0, alpha: 0.1)

b。记录每次触摸的矩形需要适应较粗的线条。而不是

    let rect = signingPath.bounds

试试,以10px的厚度为例:

    let rect = CGRect(x:signingPath.bounds.minX-5, 
    y:signingPath.bounds.minY-5, width:signingPath.bounds.maxX- 
    signingPath.bounds.minX+10, height:signingPath.bounds.maxY- 
    signingPath.bounds.minY+10)

重要提示:touchesEnded 函数还使用了 currentAnnotation 变量。您还必须在该函数中重复 rect 的定义(短函数或我上面建议的函数),并在那里重复 currentAnnotation 的定义:

    currentAnnotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)

如果您不这样做,则点击不动会使您的应用程序崩溃。

我可以验证一旦文件被保存,注释就会被保留。保存示例代码:

    let pdfData = pdfDocument?.dataRepresentation()
    let annotatedPdfUrl = URL(fileURLWithPath: "\ 
    (NSSearchPathForDirectoriesInDomains(.documentsDirectory, .userDomainMask, 
        true)[0])/AnnotatedPDF.pdf")
    try! pdfData!.write(to: annotatedPdfUrl)