PDF 手绘批注技术实现
概述
PDFHelper 的批注功能基于苹果原生 PDFKit 框架实现,未引入任何第三方 PDF 批注库。整个批注子系统约 300 行核心代码,分为三层:视图编排层、PDF 桥接层、手势处理层。
架构设计
三层结构
1 | PDFAnnotationView (SwiftUI View) |
PDFAnnotationView — SwiftUI 入口,接收 ContentViewModel 中的数据(文档、页码、工具选择、颜色),组合 PDF 视图和工具栏。
AnnotatablePDFView — UIViewRepresentable 桥接,将 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 | let annotation = PDFAnnotation(bounds: bounds, forType: .highlight, withProperties: nil) |
.highlight类型自动渲染为典型的高亮背景色.underline和.strikeOut直接使用选中颜色- 用户点击即创建,附着在触摸点位置(100x20 固定区域)
- 后续可拖拽调整位置或缩放
2. 手绘笔 (Ink) — 核心实现
手绘是批注中最复杂的部分,需要追踪连续触摸轨迹。
UIBezierPath 轨迹追踪
1 | // .began — 路径起点 |
关键点:
- 坐标转换:
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 | annotation = PDFAnnotation(bounds: ..., forType: .freeText, withProperties: nil) |
- 背景色设为
.clear实现透明底文字 fontColor独立于背景色,支持 6 色切换- 用户点击后创建可编辑文本框,支持键盘输入
- 再次点击可选中进入编辑模式
4. 形状批注 (方形 / 圆形)
1 | annotation = PDFAnnotation(bounds: bounds, forType: .square, withProperties: nil) |
.square/.circle类型直接由 PDFKit 渲染- 边框色
color+ 填充色interiorColor分开设置 - 固定 80x80 大小,用户后续可拖拽调整
5. 备注 (Note)
1 | annotation = PDFAnnotation(bounds: bounds, forType: .text, withProperties: nil) |
- 使用
.text类型图标标记,PDFKit 自动渲染为便签图标 - 点击后可查看/编辑
contents文本
颜色系统
6 色循环,采用枚举驱动:
1 | enum AnnotationColor: CaseIterable { |
工具栏中 ColorPickerView 显示 6 个圆形色块,选中标记打勾 ✓。
工具栏设计
AnnotationToolbar 使用水平滚动卡片布局:
- 8 种工具:高亮、下划线、删除线、手绘笔、文本框、方形、圆形、备注
- 每个工具 = SF Symbol 图标 + 中文名称
- 选中状态高亮蓝色背景
- 颜色按钮在末尾,展开/收起色盘
数据流
1 | 用户触摸 → Coordinator → 创建 PDFAnnotation → page.addAnnotation → 回调 onAnnotationAdded |
- 每次添加批注后触发的
onAnnotationAdded回调驱动持久化 viewModel.saveAnnotations()将当前 PDF 文档写回文件系统- 批注直接嵌入 PDF 文件——用其他 PDF 阅读器打开同样可见
长按删除
(实现位于 ContentViewModel 中,通过 UILongPressGestureRecognizer 识别)用户长按已存在的批注时:
- 检测触摸点下的
PDFAnnotation - 弹出确认提示或直接删除
page.removeAnnotation(annotation)- 重新保存文档
总结
整个批注子系统完全依赖苹果原生 PDFKit API,无第三方依赖。核心实现约 300 行 Swift 代码,涵盖 8 种批注类型。手绘部分使用 UIBezierPath + UIPanGestureRecognizer 逐点追踪轨迹,通过 PDFAnnotation.add(path) 注入 PDF 原生 Ink 标注,兼顾流畅度与兼容性。