PDF 手绘批注技术实现

作者 mcx 日期 2026-06-10
PDF 手绘批注技术实现

PDF 手绘批注技术实现

概述

PDFHelper 的批注功能基于苹果原生 PDFKit 框架实现,未引入任何第三方 PDF 批注库。整个批注子系统约 300 行核心代码,分为三层:视图编排层PDF 桥接层手势处理层

架构设计

三层结构

1
2
3
4
5
PDFAnnotationView (SwiftUI View)
↓ 数据驱动
AnnotatablePDFView (UIViewRepresentable)
↓ UIKit 桥接
Coordinator (PDFViewDelegate + 手势处理)

PDFAnnotationView — SwiftUI 入口,接收 ContentViewModel 中的数据(文档、页码、工具选择、颜色),组合 PDF 视图和工具栏。

AnnotatablePDFViewUIViewRepresentable 桥接,将 PDFView(UIKit 控件)嵌入 SwiftUI 树中。负责创建 PDFView、注册手势、同步数据。

Coordinator — 核心,实现 PDFViewDelegate 协议,处理所有触摸事件和 PDFAnnotation 的创建与添加。

为什么不用第三方库?

  • PDFKit 是 iOS 原生框架,零依赖、免维护
  • PDFAnnotation + PDFPage.addAnnotation 覆盖了所有常用批注类型
  • 避免 AliPlayerSDK 那样的二进制兼容问题
  • 用户数据始终在本地,无需联网

手势系统

双手势分离

批注交互拆为两种手势:

手势 作用范围 目标工具
UITapGestureRecognizer 所有非手绘工具 高亮、下划线、删除线、文本框、方形、圆形、备注
UIPanGestureRecognizer 仅手绘模式 手绘笔(ink)

两者在 Coordinator 中通过 handleTap(_:)handlePan(_:) 分别处理。手绘模式下 tap 被跳过(tool != .ink 提前 return),避免冲突。

手势冲突处理

UIPanGestureRecognizer 只允许单指操作:

1
panGesture.maximumNumberOfTouches = 1

PDFView 默认有自己的滚动/缩放手势,批注手势通过 Coordinator 的优先处理逻辑避免与滚动冲突——非手绘模式下 Pan 不注册,手绘模式下 Pan 优先于滚动。

批注类型实现

1. 高亮 / 下划线 / 删除线

这三种是 PDFKit 原生支持的标注类型,实现最为简单:

1
2
3
let annotation = PDFAnnotation(bounds: bounds, forType: .highlight, withProperties: nil)
annotation?.color = selectedColor.uiColor.withAlphaComponent(0.5) // 高亮半透明
page.addAnnotation(annotation)
  • .highlight 类型自动渲染为典型的高亮背景色
  • .underline.strikeOut 直接使用选中颜色
  • 用户点击即创建,附着在触摸点位置(100x20 固定区域)
  • 后续可拖拽调整位置或缩放

2. 手绘笔 (Ink) — 核心实现

手绘是批注中最复杂的部分,需要追踪连续触摸轨迹。

UIBezierPath 轨迹追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .began — 路径起点
currentPath = UIBezierPath()
currentPath?.move(to: pagePoint)

// .changed — 逐点连线
currentPath?.addLine(to: pagePoint)

// .ended — 生成 Ink 批注
let annotation = PDFAnnotation(bounds: bounds, forType: .ink, withProperties: nil)
annotation.color = selectedColor.uiColor
annotation.border = PDFBorder()
annotation.border?.lineWidth = 2
annotation.add(path) // 路径注入
page.addAnnotation(annotation)

关键点

  • 坐标转换:gesture.location(in: pdfView)pdfView.convert(location, to: page) 将屏幕坐标转为 PDF 页面坐标
  • PDFAnnotation.add(path) 将 UIBezierPath 嵌入 PDF 原生 Ink 标注
  • 边框扩展 bounds.insetBy(dx: -10, dy: -10) 避免笔迹被裁剪
  • 每次 .ended 后清空 currentPath,准备下一次绘制

触摸流畅性

  • 不降采样、不贝塞尔简化——保持原始轨迹精度
  • 直接在 UIPanGestureRecognizer.changed 逐点累加
  • 无需额外缓冲区或离屏渲染,PDFKit 的 Ink 渲染层已经硬件加速

3. 文本框 (FreeText)

1
2
3
4
annotation = PDFAnnotation(bounds: ..., forType: .freeText, withProperties: nil)
annotation?.color = .clear // 背景透明
annotation?.fontColor = selectedColor.uiColor // 文字颜色
(annotation as? PDFAnnotation)?.font = UIFont.systemFont(ofSize: 14)
  • 背景色设为 .clear 实现透明底文字
  • fontColor 独立于背景色,支持 6 色切换
  • 用户点击后创建可编辑文本框,支持键盘输入
  • 再次点击可选中进入编辑模式

4. 形状批注 (方形 / 圆形)

1
2
3
annotation = PDFAnnotation(bounds: bounds, forType: .square, withProperties: nil)
annotation?.color = selectedColor.uiColor.withAlphaComponent(0.3)
annotation?.interiorColor = selectedColor.uiColor.withAlphaComponent(0.1)
  • .square / .circle 类型直接由 PDFKit 渲染
  • 边框色 color + 填充色 interiorColor 分开设置
  • 固定 80x80 大小,用户后续可拖拽调整

5. 备注 (Note)

1
2
3
annotation = PDFAnnotation(bounds: bounds, forType: .text, withProperties: nil)
annotation?.color = selectedColor.uiColor
annotation?.contents = "备注"
  • 使用 .text 类型图标标记,PDFKit 自动渲染为便签图标
  • 点击后可查看/编辑 contents 文本

颜色系统

6 色循环,采用枚举驱动:

1
2
3
4
5
6
enum AnnotationColor: CaseIterable {
case red, yellow, green, blue, purple, orange

var color: Color { ... } // SwiftUI Color
var uiColor: UIColor { ... } // UIColor 桥接
}

工具栏中 ColorPickerView 显示 6 个圆形色块,选中标记打勾 ✓。

工具栏设计

AnnotationToolbar 使用水平滚动卡片布局:

  • 8 种工具:高亮、下划线、删除线、手绘笔、文本框、方形、圆形、备注
  • 每个工具 = SF Symbol 图标 + 中文名称
  • 选中状态高亮蓝色背景
  • 颜色按钮在末尾,展开/收起色盘

数据流

1
2
3
4
5
用户触摸 → Coordinator → 创建 PDFAnnotation → page.addAnnotation → 回调 onAnnotationAdded

viewModel.saveAnnotations()

PDFDocument.write(to:)
  • 每次添加批注后触发的 onAnnotationAdded 回调驱动持久化
  • viewModel.saveAnnotations() 将当前 PDF 文档写回文件系统
  • 批注直接嵌入 PDF 文件——用其他 PDF 阅读器打开同样可见

长按删除

(实现位于 ContentViewModel 中,通过 UILongPressGestureRecognizer 识别)用户长按已存在的批注时:

  1. 检测触摸点下的 PDFAnnotation
  2. 弹出确认提示或直接删除
  3. page.removeAnnotation(annotation)
  4. 重新保存文档

总结

整个批注子系统完全依赖苹果原生 PDFKit API,无第三方依赖。核心实现约 300 行 Swift 代码,涵盖 8 种批注类型。手绘部分使用 UIBezierPath + UIPanGestureRecognizer 逐点追踪轨迹,通过 PDFAnnotation.add(path) 注入 PDF 原生 Ink 标注,兼顾流畅度与兼容性。