iOS Swift中如何在PDFKit同时实现Apple Pencil批注与手指触控导航
我之前开发PDF批注应用时也碰到过这个痛点——频繁切换isUserInteractionEnabled确实影响体验。下面给你两种可行的方案,都能让你保留touchesBegan/Moved/Ended的批注逻辑,同时用手指正常操作PDF的平移和缩放:
方案一:自定义手势识别器+触控类型过滤
这种方法的核心是:保持PDFView的交互开启,通过自定义手势识别器处理手指的导航操作,同时过滤掉Apple Pencil的触控,让Pencil事件能传递到ViewController的touches方法里。
步骤1:添加自定义手势并禁用PDFView自带手势
在你的ViewController里添加平移和捏合手势,同时关闭PDFView自带的同类手势避免冲突:
import UIKit import PDFKit class ViewController: UIViewController { var pdfView: PDFView! private var panGesture: UIPanGestureRecognizer! private var pinchGesture: UIPinchGestureRecognizer! override func viewDidLoad() { super.viewDidLoad() pdfView = PDFView() pdfView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(pdfView) // 布局约束 NSLayoutConstraint.activate([ pdfView.leadingAnchor.constraint(equalTo: view.leadingAnchor), pdfView.trailingAnchor.constraint(equalTo: view.trailingAnchor), pdfView.topAnchor.constraint(equalTo: view.topAnchor), pdfView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) // 加载PDF let url = Bundle.main.url(forResource: "TestPDF", withExtension: "pdf")! pdfView.document = PDFDocument(url: url) pdfView.isUserInteractionEnabled = true // 保持交互开启 // 添加自定义平移手势 panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) panGesture.delegate = self pdfView.addGestureRecognizer(panGesture) // 添加自定义捏合手势 pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:))) pinchGesture.delegate = self pdfView.addGestureRecognizer(pinchGesture) // 禁用PDFView自带的平移/捏合手势,避免冲突 pdfView.gestureRecognizers?.forEach { if $0 is UIPanGestureRecognizer || $0 is UIPinchGestureRecognizer { $0.isEnabled = false } } }
步骤2:实现手势处理逻辑
模拟PDFView原生的平移和缩放行为:
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) { guard let scrollView = pdfView.documentView as? UIScrollView else { return } let translation = gesture.translation(in: scrollView) // 调整scrollView的内容偏移 scrollView.contentOffset = CGPoint( x: scrollView.contentOffset.x - translation.x, y: scrollView.contentOffset.y - translation.y ) gesture.setTranslation(.zero, in: scrollView) } @objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) { guard gesture.state == .changed else { return } // 调整PDF缩放比例 pdfView.scaleFactor *= gesture.scale gesture.scale = 1.0 }
步骤3:过滤Pencil触控
通过手势代理,只让手指触发导航手势,Pencil事件直接传递给ViewController的touches方法:
extension ViewController: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { // 只有非Pencil的触控才让手势响应 return touch.type != .stylus } }
保留你的批注逻辑
原来的touches方法可以完全保留,现在Pencil触控会正常触发这些方法:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("touchesBegan called!") if let touch = touches.first, touch.type == .stylus { print("touchesBegan pencil annotation ...") // 这里添加你的批注绘制逻辑 } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { print("touchesMoved called!") if let touch = touches.first, touch.type == .stylus { print("touchesMoved pencil annotation ...") // 这里添加你的批注绘制逻辑 } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { print("touchesEnded called!") if let touch = touches.first, touch.type == .stylus { print("touchesEnded pencil annotation ...") // 这里添加你的批注绘制逻辑 } } }
方案二:自定义PDFView子类(更推荐)
这种方法更优雅,直接在PDFView的事件传递环节区分触控类型,Pencil事件转发给ViewController处理批注,手指事件交给PDFView原生处理导航,不用自己实现平移缩放逻辑。
步骤1:自定义PDFView并添加代理
创建一个继承自PDFView的子类,通过代理将Pencil事件转发给ViewController:
import UIKit import PDFKit // 定义代理协议,用于传递Pencil触控事件 protocol PDFAnnotationDelegate: AnyObject { func handleStylusTouchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) func handleStylusTouchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) func handleStylusTouchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) } class CustomPDFView: PDFView { weak var annotationDelegate: PDFAnnotationDelegate? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first, touch.type == .stylus { // Pencil事件转发给代理(ViewController) annotationDelegate?.handleStylusTouchesBegan(touches, with: event) } else { // 手指事件交给父类处理(原生导航) super.touchesBegan(touches, with: event) } } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first, touch.type == .stylus { annotationDelegate?.handleStylusTouchesMoved(touches, with: event) } else { super.touchesMoved(touches, with: event) } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let touch = touches.first, touch.type == .stylus { annotationDelegate?.handleStylusTouchesEnded(touches, with: event) } else { super.touchesEnded(touches, with: event) } } }
步骤2:在ViewController中使用CustomPDFView
替换原来的PDFView为自定义子类,实现代理方法处理批注:
import UIKit import PDFKit class ViewController: UIViewController, PDFAnnotationDelegate { var pdfView: CustomPDFView! override func viewDidLoad() { super.viewDidLoad() pdfView = CustomPDFView() pdfView.translatesAutoresizingMaskIntoConstraints = false pdfView.annotationDelegate = self // 设置代理 view.addSubview(pdfView) // 布局约束 NSLayoutConstraint.activate([ pdfView.leadingAnchor.constraint(equalTo: view.leadingAnchor), pdfView.trailingAnchor.constraint(equalTo: view.trailingAnchor), pdfView.topAnchor.constraint(equalTo: view.topAnchor), pdfView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) // 加载PDF let url = Bundle.main.url(forResource: "TestPDF", withExtension: "pdf")! pdfView.document = PDFDocument(url: url) pdfView.isUserInteractionEnabled = true // 保持交互开启 } // 实现代理方法,处理Pencil批注 func handleStylusTouchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("touchesBegan called!") if let touch = touches.first { print("touchesBegan pencil annotation ...") // 这里添加你的批注绘制逻辑 } } func handleStylusTouchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { print("touchesMoved called!") if let touch = touches.first { print("touchesMoved pencil annotation ...") // 这里添加你的批注绘制逻辑 } } func handleStylusTouchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { print("touchesEnded called!") if let touch = touches.first { print("touchesEnded pencil annotation ...") // 这里添加你的批注绘制逻辑 } } }
为什么这两种方案可行?
原来的问题在于:当PDFView.isUserInteractionEnabled = true时,它内部的UIScrollView会优先拦截所有触控事件,导致ViewController的touches方法无法触发。这两种方案都是通过在事件传递链的早期区分触控类型,让Pencil事件绕过PDFView的拦截,直接到达你的批注逻辑,同时保留手指对PDF的原生导航操作。
个人更推荐方案二,因为它不需要自己实现平移缩放的细节,完全复用PDFView的原生行为,代码更简洁也更稳定。
内容的提问来源于stack exchange,提问作者Lars

