关于 pdfkit
面向 Apple 平台开发者的 PDFKit 使用指南,涵盖 PDF 显示、文档查询与编辑。内容也说明 SwiftUI 集成及常见线程和坐标问题。
主要功能
- 01显示、缩放与导航 PDF
- 02搜索和提取文档文本
- 03创建及管理 PDF 注释
- 04生成页面缩略图
- 05集成 SwiftUI PDFView
适用场景
- 01在应用中呈现可导航的 PDF
- 02搜索大型 PDF 并逐项处理结果
- 03为 PDF 页面添加高亮或文本注释
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill pdfkit安装后在 Claude Code 中输入/pdfkit确认已加载。
附带文件evals/evals.jsonreferences/pdfkit-patterns.md
详细介绍
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) {
// Show password prompt
}
}
保存和页面操作
document.write(to: outputURL)
document.write(to: outputURL, withOptions: [
.ownerPasswordOption: "ownerPass", .userPasswordOption: "userPass"
])
let data = document.dataRepresentation()
// Pages are zero-based. Validate indices; out-of-range calls raise exceptions.
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 提供内置导航,并跟踪浏览历史。
// Go to a specific page
let pageIndex = 5
if let document = pdfView.document,
pageIndex >= 0,
pageIndex < document.pageCount,
let page = document.page(at: pageIndex) {
pdfView.go(to: page)
}
// Sequential navigation
pdfView.goToNextPage(nil)
pdfView.goToPreviousPage(nil)
pdfView.goToFirstPage(nil)
pdfView.goToLastPage(nil)
// Check navigation state
if pdfView.canGoToNextPage { /* ... */ }
// History navigation
if pdfView.canGoBack { pdfView.goBack(nil) }
// Go to a specific point on the current page
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(_:) 以处理搜索完成事件。
增量搜索与查找交互
// Find next match from current selection
let next = document.findString("term", fromSelection: current, withOptions: [.caseInsensitive])
// System find bar; apply the Setup availability gate
pdfView.isFindInteractionEnabled = true
文本提取
let fullText = document.string // Entire document
let firstPage = document.pageCount > 0 ? document.page(at: 0) : nil
let pageText = firstPage?.string // Single page
let attributed = firstPage?.attributedString // With formatting
// Region-based extraction
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 = "This is a sticky note."
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 = "Added commentary"
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)
// Internal page 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)
// All pages
let thumbnails = (0..<document.pageCount).compactMap {
document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}
SwiftUI 集成
在 SwiftUI 中使用时,将 PDFView 包装在 UIViewRepresentable 中。配置
PDFView、页面、批注、搜索、缩略图或覆盖视图的 PDF 专用包装器
属于此技能范围;只有通用的 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("Unable to load PDF", systemImage: "doc.questionmark")
}
}
}
如需支持页面跟踪、批注命中检测和协调器模式的交互式包装器,请参阅 references/pdfkit-patterns.md。
页面覆盖视图
PDFPageOverlayViewProvider 可在单个页面上方放置 UIKit 视图,
用于交互式控件或标准批注之外的自定义渲染。
class OverlayProvider: NSObject, PDFPageOverlayViewProvider {
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let overlay = UIView()
// Add custom subviews
return overlay
}
}
class PDFOverlayController: UIViewController {
let pdfView = PDFView()
private let overlayProvider = OverlayProvider()
override func viewDidLoad() {
super.viewDidLoad()
pdfView.pageOverlayViewProvider = overlayProvider
}
}
pageOverlayViewProvider 是弱引用,因此请确保强引用持有该提供程序。有关覆盖视图生命周期和保存处理,请阅读 references/pdfkit-patterns.md。
常见错误
不要:强制解包 PDFDocument 初始化结果
PDFDocument(url:) 和 PDFDocument(data:) 是可失败的初始化方法。
// WRONG
let document = PDFDocument(url: url)!
// CORRECT
guard let document = PDFDocument(url: url) else { return }
不要:忘记在 PDFView 上启用 autoScales
如果未启用 autoScales,PDF 将按原始分辨率渲染。
// WRONG
pdfView.document = document
// CORRECT
pdfView.autoScales = true
pdfView.document = document
不要:忽略批注中的 PDF 坐标系
PDF 页面坐标原点位于左下角,Y 轴向上递增——与 UIKit 相反。
// WRONG: UIKit coordinates
let bounds = CGRect(x: 50, y: 50, width: 200, height: 30)
// CORRECT: PDF coordinates (origin bottom-left)
let pageBounds = page.bounds(for: .mediaBox)
let pdfY = pageBounds.height - 50 - 30
let bounds = CGRect(x: 50, y: pdfY, width: 200, height: 30)
不要:在后台线程修改注释
PDFKit 类不是线程安全的。
// WRONG
DispatchQueue.global().async { page.addAnnotation(annotation) }
// CORRECT
DispatchQueue.main.async { page.addAnnotation(annotation) }
不要:在 UIViewRepresentable 中使用 == 比较 PDFDocument
PDFDocument 是引用类型。请使用身份比较(!==)。
// WRONG: Always replaces document
func updateUIView(_ pdfView: PDFView, context: Context) {
pdfView.document = document
}
// CORRECT
func updateUIView(_ pdfView: PDFView, context: Context) {
if pdfView.document !== document {
pdfView.document = document
}
}
审查清单
-
PDFDocument初始化使用可选绑定,而不是强制解包 - 设置
pdfView.autoScales = true,以确保正确的初始显示效果 - 访问页面前,根据
pageCount检查页面索引 - 配置
displayMode和displayDirection,使其符合设计要求 - 注释使用 PDF 坐标空间(原点位于左下角,Y 轴向上)
- 所有 PDFKit 修改都在主线程上进行
- 使用
isLocked/unlock(withPassword:)处理受密码保护的 PDF - SwiftUI 包装器在
updateUIView中使用!==身份检查 - 监听
PDFViewPageChanged通知以跟踪页面 - 将
PDFThumbnailView.pdfView关联到主PDFView - 大型文档搜索使用带委托的异步
beginFindString - 需要加密时,保存文档使用
write(to:withOptions:)
参考资料
- 扩展模式(表单、水印、合并、打印、叠加层、大纲、自定义绘制):references/pdfkit-patterns.md
- PDFKit 框架
- PDFView
- PDFDocument
- PDFPage、PDFAnnotation、PDFSelection、PDFThumbnailView
- PDFPageOverlayViewProvider
- 向 PDF 文档添加小组件
- 向 PDF 添加自定义图形