mcpField
P
pdfkit

pdfkit

github.com/dpearson2699文档处理1.2k3.2k 次安装13 周前更新
代码仓库

关于 pdfkit

面向 Apple 平台开发者的 PDFKit 使用指南,涵盖 PDF 显示、文档查询与编辑。内容也说明 SwiftUI 集成及常见线程和坐标问题。

主要功能

  1. 01显示、缩放与导航 PDF
  2. 02搜索和提取文档文本
  3. 03创建及管理 PDF 注释
  4. 04生成页面缩略图
  5. 05集成 SwiftUI PDFView

适用场景

  1. 01在应用中呈现可导航的 PDF
  2. 02搜索大型 PDF 并逐项处理结果
  3. 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:)

参考资料

评论

支持 Markdown