PDFKit
使用 PDFView、PDFDocument、PDFPage、PDFAnnotation 和 PDFSelection 顯示、導覽、搜尋、註解和操作 PDF 文件。
目標為 Swift 6.3 / iOS 26+。
目錄
設定
PDFKit 不需要任何授權或 Info.plist 項目。
import PDFKit
平台可用性: iOS 11+、iPadOS 11+、Mac Catalyst 13.1+、macOS 10.4+、tvOS 11+、visionOS 1.0+。
顯示 PDF
PDFView 是 UIView 的子類別,可直接呈現 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)
document.removePage(at: 2)
document.exchangePage(at: 0, withPageAt: 3)
頁面導覽
PDFView 內建導覽功能,並包含歷史記錄。
// 前往特定頁面
if let page = pdfView.document?.page(at: 5) {
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) }
// 前往頁面上的特定位置
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 = "第 \(index + 1) 頁,共 \(doc.pageCount) 頁"
}
文字搜尋與選取
同步搜尋
let results: [PDFSelection] = document.findString(
"搜尋字詞", withOptions: [.caseInsensitive]
)
非同步搜尋
對於大型文件,使用 PDFDocumentDelegate 進行背景搜尋。
實作 didMatchString(_:) 來接收每個匹配結果,以及
documentDidEndDocumentFind(_:) 來處理完成事件。
增量搜尋與查找互動
// 從當前選取範圍尋找下一個匹配
let next = document.findString("term", fromSelection: current, withOptions: [.caseInsensitive])
// 系統查找列 (iOS 16+)
pdfView.isFindInteractionEnabled = true
文字擷取
let fullText = document.string // 整份文件
let pageText = document.page(at: 0)?.string // 單頁
let attributed = document.page(at: 0)?.attributedString // 包含格式
// 基於區域的擷取
if let page = document.page(at: 0) {
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 | .highlight | 文字標示 (黃色螢光筆) |
| Underline | .underline | 文字標示 (底線) |
| StrikeOut | .strikeOut | 文字標示 (刪除線) |
| Text | .text | 便利貼圖示 |
| FreeText | .freeText | 內嵌文字區塊 |
| Ink | .ink | 手繪路徑 |
| Link | .link | URL 或頁面目的地 |
| Line | .line | 具有端點的直線 |
| Square | .square | 矩形形狀 |
| Circle | .circle | 橢圓形狀 |
| Stamp | .stamp | 橡皮圖章 (已核准等) |
| Widget | .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 整合
將 PDFView 包裝在 UIViewRepresentable 中以供 SwiftUI 使用。
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")
}
}
}
關於包含頁面追蹤、註解點擊偵測和協調者模式的互動式包裝器,請參閱 references/pdfkit-patterns.md。
頁面覆層 (iOS 16+)
PDFPageOverlayViewProvider 可在個別頁面頂部放置 UIKit 檢視,
用於互動控制或超出標準註解的自訂渲染。
class OverlayProvider: NSObject, PDFPageOverlayViewProvider {
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let overlay = UIView()
// 加入自訂子檢視
return overlay
}
}
pdfView.pageOverlayViewProvider = overlayProvider
常見錯誤
不要:強制解包 PDFDocument 初始化
PDFDocument(url:) 和 PDFDocument(data:) 是可失敗的初始化器。
// 錯誤
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 類別不具線程安全性。
// 錯誤
DispatchQueue.global().async { page.addAnnotation(annotation) }
// 正確
DispatchQueue.main.async { page.addAnnotation(annotation) }
不要:在 UIViewRepresentable 中使用 == 比較 PDFDocument
PDFDocument 是參考型別。應使用身分比較 (!==)。
// 錯誤:總是替換文件
func updateUIView(_ pdfView: PDFView, context: Context) {
pdfView.document = document
}
// 正確
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 文件加入 Widget
- 為 PDF 加入自訂圖形