pdfkit

pdfkit

热门

使用 PDFKit 展示和操作 PDF 文档。适用场景包括:嵌入 PDFView 显示 PDF 文件、创建或修改 PDFDocument 实例、添加标注(高亮、批注、签名组件)、使用 PDFSelection 提取文本、翻页导航、生成缩略图、填写 PDF 表单,以及在 SwiftUI 中封装 PDFView。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
pdfkit
描述

使用 PDFKit 展示和操作 PDF 文档。适用场景包括:嵌入 PDFView 显示 PDF 文件、创建或修改 PDFDocument 实例、添加标注(高亮、批注、签名组件)、使用 PDFSelection 提取文本、翻页导航、生成缩略图、填写 PDF 表单,以及在 SwiftUI 中封装 PDFView。

PDFKit

利用 PDFViewPDFDocumentPDFPagePDFAnnotation 以及 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 -->