【问题标题】:Implement ink annotations on iOS 11 PDFKit document在 iOS 11 PDFKit 文档上实现墨迹注释
【发布时间】:2018-04-18 13:06:54
【问题描述】:

我想让用户在 PDFView 中查看的 iOS 11 PDFKit 文档上绘图。绘图最终应该嵌入到 PDF 中。

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

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

我尝试在 PDFView 和 PDFPage 上覆盖 touchesBegan,但它从未被调用。我尝试添加 UIGestureRecognizer,但没有完成任何操作。

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

【问题讨论】:

    标签: ios pdf touch ios11


    【解决方案1】:

    我通过创建一个新的视图类(例如 Annotate View)并在用户注释时放置在 PDFView 之上来完成此操作。

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

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

    我的主要课程

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

    【讨论】:

    • 很抱歉 - 我已经创建了一个不需要在顶部添加视图的解决方案。您的解决方案不太理想,因为这意味着 PDF 必须是单页且不能滚动 - 否则注释将出现在错误的位置。
    【解决方案2】:

    最后我通过创建一个扩展 UIViewController 和 UIGestureRecognizerDelegate 的 PDFViewController 类解决了这个问题。我添加了一个 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) 值。

    【讨论】:

    • 不确定UIGraphicsBeginImageContext(CGSize(width: 800, height: 600)) 是否在此代码中。
    • 抱歉,我没有准备要在此处发布的代码 - 我觉得我必须在其他人感兴趣的情况下发布我的代码,因此该示例确实可能包含不需要的代码此处为示例,但用于我的应用程序的其他目的(尚未完成)。据我记得那行代码在那里,因为我收到了没有上下文启动的警告。
    • 对不起,如果您误解了我,这不是负面评论,而是作为一个问题。我想知道为什么需要它,它的用途是什么。我为 PDFView 做了我自己的小 UIGestureRecognizer,我可以确认没有它它也能工作(在模拟器上没有看到警告。也许在设备上,它是不同的)
    • 据我记得不是 UIGestureRecognizer 代码需要它,而是当我想在用户在 iPad 上绘图/书写时实际在文档上绘制注释时。我一直在使用真实设备和 Apple Pencil 进行测试 - 效果很好!
    • moveCenter 在 swift4 中出现错误。我该怎么办?
    【解决方案3】:

    编辑:

    jksoegaard 的回答虽然是我在这个问题上所有工作的灵感来源,但有一个缺陷:在 touchMoved 期间,会创建多个 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 之前,您都需要(根据该函数的需要使用 annotation 或 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)
    

    【讨论】:

    • 如果还有人需要解决这个任务,我创建了一个完整的教程:medium.com/@artempoluektov/…
    • @zramled - 请参阅我帖子开头的编辑。
    • 嘿,Ron,感谢您的项目,虽然性能有了显着提高,但我在缩放 pdf 时仍然看到很多延迟,是否有更多技巧或技术来克服这个内存问题。由于我公司的要求,我非常需要克服这个特殊问题。当我使用 ipad 而不是 pro 时,这个问题会变得更糟......谢谢。
    • 亲爱的 Raj,只是为了让碰巧看到您评论的人继续跟进 - 我们在这里讨论过:github.com/ClassicalDude/pdfView-Freedraw/issues/3。 TL;DR:问题出在 PDFKit 上,而不是框架上。由 PDFKit 渲染时,带有大量注释的 PDF 速度缓慢且占用大量内存。我的框架适用于少量注释。如果开发人员绝对需要能够处理带有大量注释的 PDF,我建议将注释保留为 CAShapeLayers,直到您保存并关闭文档,然后才将注释提交到 PDF。
    猜你喜欢
    • 2018-04-06
    • 1970-01-01
    • 2018-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-02
    • 2020-02-04
    • 1970-01-01
    相关资源
    最近更新 更多