使用 PDFKit 展示和操作 PDF 文档。适用场景包括:嵌入 PDFView 显示 PDF 文件、创建或修改 PDFDocument 实例、添加标注(高亮、批注、签名组件)、使用 PDFSelection 提取文本、翻页导航、生成缩略图、填写 PDF 表单,以及在 SwiftUI 中封装 PDFView。
PDFKit
利用 PDFView、PDFDocument、PDFPage、PDFAnnotation 以及 PDFSelection 实现 PDF 文档的展示、导航、搜索、标注与各种编辑操作。
目录
环境准备
PDFKit 无需配置任何权限项或 Info.plist 属性。
import PDFKit
| API | 支持版本 |
|---|---|
| PDFKit 框架 | iOS/iPadOS/tvOS 11+、Mac Catalyst 13.1+、macOS 10.4+、visionOS 1.0+ |
| 查找交互与页面图层覆盖 | iOS/iPadOS 16+ |
展示 PDF
PDFView 负责渲染 PDF 内容,并自动处理缩放、滚动、文本选取以及翻页导航。
import PDFKit
import UIKit
class PDFViewController: UIViewController {
let pdfView = PDFView()
override func viewDidLoad() {
super.viewDidLoad()
pdfView.frame = view.bounds
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(pdfView)
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
pdfView.displayDirection = .vertical
if let url = Bundle.main.url(forResource: "sample", withExtension: "pdf") {
pdfView.document = PDFDocument(url: url)
}
}
}
显示模式
| 模式 | 行为 |
|---|---|
.singlePage |
单页显示 |
.singlePageContinuous |
连续单页(垂直堆叠,支持滚动) |
.twoUp |
双页并排显示 |
.twoUpContinuous |
连续双页(双页并排且可连续滚动) |
缩放与外观设置
pdfView.autoScales = true
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0
pdfView.displaysPageBreaks = true
pdfView.pageShadowsEnabled = true
pdfView.interpolationQuality = .high
加载文档
PDFDocument 支持从 URL、Data 加载,也支持直接创建空白文档。
let fileDoc = PDFDocument(url: fileURL)
let dataDoc = PDFDocument(data: pdfData)
let emptyDoc = PDFDocument()
密码保护的 PDF
guard let document = PDFDocument(url: url) else { return }
if document.isLocked {
if !document.unlock(withPassword: userPassword) {
// 弹出密码输入提示框
}
}
保存与页面操作
document.write(to: outputURL)
document.write(to: outputURL, withOptions: [
.ownerPasswordOption: "ownerPass", .userPasswordOption: "userPass"
])
let data = document.dataRepresentation()
// 页码从 0 开始。请务必检查索引范围,越界调用会抛出异常。
let count = document.pageCount
document.insert(PDFPage(), at: count)
if document.pageCount > 2 {
document.removePage(at: 2)
}
if document.pageCount > 3 {
document.exchangePage(at: 0, withPageAt: 3)
}
页面导航
PDFView 内置了导航及历史记录追踪功能。
// 跳转到指定页面
let pageIndex = 5
if let document = pdfView.document,
pageIndex >= 0,
pageIndex < document.pageCount,
let page = document.page(at: pageIndex) {
pdfView.go(to: page)
}
// 顺序翻页
pdfView.goToNextPage(nil)
pdfView.goToPreviousPage(nil)
pdfView.goToFirstPage(nil)
pdfView.goToLastPage(nil)
// 检查导航状态
if pdfView.canGoToNextPage { /* ... */ }
// 历史记录返回
if pdfView.canGoBack { pdfView.goBack(nil) }
// 跳转到当前页面的特定位置
if let page = pdfView.currentPage {
let destination = PDFDestination(page: page, at: CGPoint(x: 0, y: 500))
pdfView.go(to: destination)
}
监听切页事件
NotificationCenter.default.addObserver(
self, selector: #selector(pageChanged),
name: .PDFViewPageChanged, object: pdfView
)
@objc func pageChanged(_ notification: Notification) {
guard let page = pdfView.currentPage,
let doc = pdfView.document else { return }
let index = doc.index(for: page)
pageLabel.text = "Page \(index + 1) of \(doc.pageCount)"
}
文本搜索与选择
同步搜索
let results: [PDFSelection] = document.findString(
"search term", withOptions: [.caseInsensitive]
)
异步搜索
在处理大型文档时,推荐使用 PDFDocumentDelegate 进行后台搜索。
实现 didMatchString(_:) 实时接收匹配项,并监听 documentDidEndDocumentFind(_:) 确定搜索完成。
增量搜索与查找交互
// 从当前选中位置继续查找下一个匹配项
let next = document.findString("term", fromSelection: current, withOptions: [.caseInsensitive])
// 启用系统内置查找栏;需满足环境准备中的系统版本要求
pdfView.isFindInteractionEnabled = true
文本提取
let fullText = document.string // 提取整个文档内容
let firstPage = document.pageCount > 0 ? document.page(at: 0) : nil
let pageText = firstPage?.string // 提取单页文本
let attributed = firstPage?.attributedString // 提取带有格式的富文本
// 按区域提取文本
if let page = firstPage {
let selection = page.selection(for: CGRect(x: 50, y: 50, width: 400, height: 200))
let text = selection?.string
}
高亮搜索结果
let results = document.findString("important", withOptions: [.caseInsensitive])
for selection in results { selection.color = .yellow }
pdfView.highlightedSelections = results
if let first = results.first {
pdfView.setCurrentSelection(first, animate: true)
pdfView.go(to: first)
}
标注
标注使用 PDFAnnotation(bounds:forType:withProperties:) 初始化,并添加至具体的 PDFPage 中。
高亮标注
func addHighlight(to page: PDFPage, selection: PDFSelection) {
let highlight = PDFAnnotation(
bounds: selection.bounds(for: page),
forType: .highlight, withProperties: nil
)
highlight.color = UIColor.yellow.withAlphaComponent(0.5)
page.addAnnotation(highlight)
}
便签批注
let note = PDFAnnotation(
bounds: CGRect(x: 100, y: 700, width: 30, height: 30),
forType: .text, withProperties: nil
)
note.contents = "这是一条文本批注。"
note.color = .systemYellow
note.iconType = .comment
page.addAnnotation(note)
自由文本标注
let freeText = PDFAnnotation(
bounds: CGRect(x: 50, y: 600, width: 300, height: 40),
forType: .freeText, withProperties: nil
)
freeText.contents = "补充说明文字"
freeText.font = UIFont.systemFont(ofSize: 14)
freeText.fontColor = .darkGray
page.addAnnotation(freeText)
链接标注
let link = PDFAnnotation(
bounds: CGRect(x: 50, y: 500, width: 200, height: 20),
forType: .link, withProperties: nil
)
link.url = URL(string: "https://example.com")
page.addAnnotation(link)
// 页内跳转链接
link.destination = PDFDestination(page: targetPage, at: .zero)
移除标注
for annotation in page.annotations {
page.removeAnnotation(annotation)
}
常见的标注子类型包括:.highlight(高亮)、.underline(下划线)、.strikeOut(删除线)、.text(便签)、.freeText(自由文本)、.ink(手写笔迹)、.link(链接)、.line(直线)、.square(矩形)、.circle(圆形)、.stamp(印章)以及 .widget(表单控件)。
缩略图
PDFThumbnailView
PDFThumbnailView 用于展示绑定到 PDFView 的页面缩略图栏。
let thumbnailView = PDFThumbnailView()
thumbnailView.pdfView = pdfView
thumbnailView.thumbnailSize = CGSize(width: 60, height: 80)
thumbnailView.layoutMode = .vertical
thumbnailView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(thumbnailView)
代码编程式生成缩略图
let thumbnail = page.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
// 生成全文档各页缩略图
let thumbnails = (0..<document.pageCount).compactMap {
document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}
SwiftUI 集成
在 SwiftUI 中使用 UIViewRepresentable 封装 PDFView。专门用于配置 PDFView、页面、标注、搜索、缩略图或图层覆盖的 PDF 封装实现归属于本 Skill;如果是通用的 Representable 生命周期、布局或 SwiftUI 状态架构问题,请参考 SwiftUI/UIKit 互操作指南。
import SwiftUI
import PDFKit
struct PDFKitView: UIViewRepresentable {
let document: PDFDocument
func makeUIView(context: Context) -> PDFView {
let pdfView = PDFView()
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
pdfView.document = document
return pdfView
}
func updateUIView(_ pdfView: PDFView, context: Context) {
if pdfView.document !== document {
pdfView.document = document
}
}
}
使用示例
struct DocumentScreen: View {
let url: URL
var body: some View {
if let document = PDFDocument(url: url) {
PDFKitView(document: document)
.ignoresSafeArea()
} else {
ContentUnavailableView("无法加载 PDF", systemImage: "doc.questionmark")
}
}
}
有关包含页面追踪、标注点击检测(Hit detection)和 Coordinator 模式的交互式封装,请参阅 references/pdfkit-patterns.md。
页面图层覆盖
PDFPageOverlayViewProvider 用于在单个 PDF 页面上悬浮叠加 UIKit 视图,以便实现交互控件或超越标准标注的自定义渲染。
class OverlayProvider: NSObject, PDFPageOverlayViewProvider {
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let overlay = UIView()
// 添加自定义子视图
return overlay
}
}
class PDFOverlayController: UIViewController {
let pdfView = PDFView()
private let overlayProvider = OverlayProvider()
override func viewDidLoad() {
super.viewDidLoad()
pdfView.pageOverlayViewProvider = overlayProvider
}
}
pageOverlayViewProvider 属性为弱引用(weak),因此必须保持 Provider 对象的强引用。关于图层覆盖的生命周期与保存处理,请阅读 references/pdfkit-patterns.md。
常见踩坑点
错误做法:强行解包 PDFDocument 初始化器
PDFDocument(url:) 与 PDFDocument(data:) 为可失败初始化器(failable initializer)。
// ❌ 错误
let document = PDFDocument(url: url)!
// ✅ 正确
guard let document = PDFDocument(url: url) else { return }
错误做法:漏设 PDFView 的 autoScales
如果不启用 autoScales,PDF 会直接按原始分辨率渲染。
// ❌ 错误
pdfView.document = document
// ✅ 正确
pdfView.autoScales = true
pdfView.document = document
错误做法:在标注中忽视 PDF 坐标系
PDF 页面的坐标系原点位于左下角,且 Y 轴向上递增——与 UIKit 的坐标系完全相反。
// ❌ 错误:使用 UIKit 坐标系
let bounds = CGRect(x: 50, y: 50, width: 200, height: 30)
// ✅ 正确:使用 PDF 坐标系(原点在左下角)
let pageBounds = page.bounds(for: .mediaBox)
let pdfY = pageBounds.height - 50 - 30
let bounds = CGRect(x: 50, y: pdfY, width: 200, height: 30)
错误做法:在后台线程修改标注
PDFKit 相关类均为非线程安全(not thread-safe)。
// ❌ 错误
DispatchQueue.global().async { page.addAnnotation(annotation) }
// ✅ 正确
DispatchQueue.main.async { page.addAnnotation(annotation) }
错误做法:在 UIViewRepresentable 中使用 == 比较 PDFDocument
PDFDocument 是引用类型,应用恒等运算符(!==)进行比较。
// WRONG: Alway
<!-- truncated for translation batch; full body continues in source -->




