You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何在.onAppear()内添加手势实现PDF批注拖拽?

解决SwiftUI中PDF批注的拖拽问题

问题核心

你遇到的三个关键问题:

  • SwiftUI视图结构体无法用@objc修饰方法,导致UIPanGestureRecognizer无法绑定动作
  • 无法在onAppear中为PDF批注添加SwiftUI的DragGesture
  • 自行实现的拖拽逻辑中,批注移动速度与手指/鼠标速度不匹配

方案一:用UIViewRepresentable封装PDFView,使用UIPanGestureRecognizer

SwiftUI结构体的限制可以通过UIViewRepresentable绕开——把PDFView封装成UIKit组件,在协调器(NSObject子类)中处理手势,天然支持@objc方法。

1. 封装PDFView组件

import SwiftUI
import PDFKit

struct PDFContainerView: UIViewRepresentable {
    @Binding var document: PDFDocument?
    @Binding var selectedAnnotation: PDFAnnotation?
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        pdfView.document = document
        // 添加平移手势
        let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handlePan(_:)))
        pdfView.addGestureRecognizer(panGesture)
        return pdfView
    }
    
    func updateUIView(_ uiView: PDFView, context: Context) {
        uiView.document = document
    }
    
    // 协调器:处理手势逻辑
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject {
        var parent: PDFContainerView
        
        init(_ parent: PDFContainerView) {
            self.parent = parent
        }
        
        @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
            guard let pdfView = gesture.view as? PDFView,
                  let page = pdfView.currentPage else { return }
            
            switch gesture.state {
            case .began:
                // 识别点击位置的批注
                let location = gesture.location(in: pdfView)
                let hitResult = pdfView.hitTest(location, with: [.annotation])
                parent.selectedAnnotation = hitResult.annotation
            case .changed:
                guard let annotation = parent.selectedAnnotation else { return }
                // 获取手势位移,转换为PDF页面坐标系
                let translation = gesture.translation(in: pdfView)
                // 修正Y轴方向(PDF坐标系向上,UI坐标系向下)
                let newOrigin = CGPoint(
                    x: annotation.bounds.origin.x + translation.x,
                    y: annotation.bounds.origin.y - translation.y
                )
                annotation.bounds.origin = newOrigin
                // 重置位移,避免累计偏差
                gesture.setTranslation(.zero, in: pdfView)
            case .ended, .cancelled:
                parent.selectedAnnotation = nil
            default: break
            }
        }
    }
}

2. 主视图中使用组件

struct WelcomeView: View {
    @State private var document: PDFDocument?
    @State private var selectedAnnotation: PDFAnnotation?
    @State private var annotationImage: UIImage?
    
    init() {
        if let url = Bundle.main.url(forResource: "Test05", withExtension: "pdf") {
            self.document = PDFDocument(url: url)
        }
        self.annotationImage = UIImage(named: "testSig.png")
    }
    
    var body: some View {
        VStack {
            PDFContainerView(document: $document, selectedAnnotation: $selectedAnnotation)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            Button("Add Annotation") {
                addImageAnnotation()
            }
        }
    }
    
    private func addImageAnnotation() {
        guard let annImage = annotationImage,
              let document = document,
              let page = document.page(at: 0) else { return }
        
        let pageBounds = page.bounds(for: .cropBox)
        let annotationBounds = CGRect(
            x: pageBounds.midX - 100,
            y: pageBounds.midY - 50,
            width: 200,
            height: 100
        )
        let annotation = ImageAnnotation(image: annImage, bounds: annotationBounds, properties: nil)
        page.addAnnotation(annotation)
    }
}

// 自定义批注类保持不变
class ImageAnnotation: PDFAnnotation {
    private let image: UIImage
    
    init(image: UIImage, bounds: CGRect, properties: [AnyHashable: Any]?) {
        self.image = image
        super.init(bounds: bounds, forType: PDFAnnotationSubtype.stamp, withProperties: properties)
    }
    
    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("required by PDFAnnotation - not needed for Image")
    }
    
    override func draw(with box: PDFDisplayBox, in context: CGContext) {
        guard let cgImage = image.cgImage else { return }
        context.draw(cgImage, in: bounds)
    }
}

方案二:修正现有DragGesture的速度不匹配问题

你当前实现的问题根源:一是直接使用DragGesture的累计translation计算位移,二是未考虑PDFView的缩放比例,导致屏幕坐标与PDF页面坐标错位。调整如下:

修正后的代码片段

struct WelcomeView: View {
    @State private var pdfContainer: PDFView
    @State private var annotationImage: UIImage?
    @State private var selectedAnnotation: PDFAnnotation?
    @State private var initialAnnotationPos: CGPoint?
    @State private var initialDragPos: CGPoint?

    init(annotationImage: UIImage? = UIImage(named: "testSig.png")) {
        self.pdfContainer = PDFView()
        if let url = Bundle.main.url(forResource: "Test05", withExtension: "pdf") {
            pdfContainer.document = PDFDocument(url: url)
        }
        pdfContainer.autoScales = true
        self.annotationImage = annotationImage
    }

    var body: some View {
        VStack {
            DocViewPDF(doc: pdfContainer.document)
                .gesture(DragGesture(minimumDistance: 0)
                    .onChanged { g in
                        guard let page = pdfContainer.currentPage,
                              let annotation = selectedAnnotation else { return }
                        
                        if initialDragPos == nil {
                            // 记录拖拽起始状态
                            initialDragPos = g.location
                            initialAnnotationPos = annotation.bounds.origin
                        }
                        
                        // 计算手指相对位移
                        let deltaX = g.location.x - initialDragPos!.x
                        let deltaY = g.location.y - initialDragPos!.x
                        
                        // 转换为PDF页面坐标(修正缩放和Y轴方向)
                        let pdfDeltaX = deltaX / pdfContainer.scaleFactor
                        let pdfDeltaY = -deltaY / pdfContainer.scaleFactor
                        
                        // 更新批注位置
                        annotation.bounds.origin = CGPoint(
                            x: initialAnnotationPos!.x + pdfDeltaX,
                            y: initialAnnotationPos!.y + pdfDeltaY
                        )
                    }
                    .onEnded { _ in
                        initialDragPos = nil
                        initialAnnotationPos = nil
                    })
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            Button("Add Annotation") {
                addImageAnnotation()
            }
        }
        .onAppear {
            addImageAnnotation()
        }
    }
    
    private func addImageAnnotation() {
        guard let annImage = annotationImage,
              let page = pdfContainer.currentPage else { return }
        
        let pageBounds = page.bounds(for: .cropBox)
        let annotationBounds = CGRect(
            x: pageBounds.midX - 100,
            y: pageBounds.midY - 50,
            width: 200,
            height: 100
        )
        selectedAnnotation = ImageAnnotation(image: annImage, bounds: annotationBounds, properties: nil)
        page.addAnnotation(selectedAnnotation!)
    }
}

关键修复点

  • 记录拖拽起始时的手指位置和批注位置,计算相对位移而非累计总位移
  • 通过pdfContainer.scaleFactor将屏幕坐标转换为PDF页面实际坐标,解决缩放导致的速度差
  • 修正PDF与UI坐标系的Y轴方向差异

思路总结

  • 偏好UIKit手势:用UIViewRepresentable封装PDFView,在协调器中处理UIPanGestureRecognizer,彻底避开SwiftUI结构体的限制
  • 坚持SwiftUI手势:必须处理坐标转换和缩放比例,避免位移计算偏差
  • 无论哪种方案,都要注意PDF坐标系(Y轴向上)与UI坐标系(Y轴向下)的差异

内容的提问来源于stack exchange,提问作者Simon Lemcke

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 17:59:57