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
相关产品推荐
相关产品推荐

