使用 PDFKit 顯示與操作 PDF 文件。適用於在 app 中嵌入 PDFView 展示 PDF 檔案、建立或修改 PDFDocument 實例、新增標註(螢光筆高亮標示、筆記、簽名元件)、透過 PDFSelection 擷取文字、頁面導覽、產生縮圖、填寫 PDF 表單,或在 SwiftUI 中封裝 PDFView 等情境。
PDFKit
使用 PDFView、PDFDocument、PDFPage、PDFAnnotation 和 PDFSelection 來顯示、導覽、搜尋、標註及操作 PDF 文件。
Contents
Setup
PDFKit 不需要設定額外的權限(Entitlements)或 Info.plist 項目。
import PDFKit
| API | 可用性 |
|---|---|
| PDFKit 框架 | iOS/iPadOS/tvOS 11+、Mac Catalyst 13.1+、macOS 10.4+、visionOS 1.0+ |
| 搜尋互動(Find interaction)與頁面覆蓋層(Page overlays) | iOS/iPadOS 16+ |
Displaying PDFs
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)
}
}
}
Display Modes
| 模式 | 行為 |
|---|---|
.singlePage |
一次顯示單一頁面 |
.singlePageContinuous |
頁面垂直堆疊,可連續捲動 |
.twoUp |
雙頁並排顯示 |
.twoUpContinuous |
雙頁並排且可連續捲動 |
Scaling and Appearance
pdfView.autoScales = true
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0
pdfView.displaysPageBreaks = true
pdfView.pageShadowsEnabled = true
pdfView.interpolationQuality = .high
Loading Documents
PDFDocument 可透過 URL、Data 載入,或建立為空白文件。
let fileDoc = PDFDocument(url: fileURL)
let dataDoc = PDFDocument(data: pdfData)
let emptyDoc = PDFDocument()
Password-Protected PDFs
guard let document = PDFDocument(url: url) else { return }
if document.isLocked {
if !document.unlock(withPassword: userPassword) {
// 顯示密碼輸入提示
}
}
Saving and Page Manipulation
document.write(to: outputURL)
document.write(to: outputURL, withOptions: [
.ownerPasswordOption: "ownerPass", .userPasswordOption: "userPass"
])
let data = document.dataRepresentation()
// 頁面索引從 0 開始。請驗證索引範圍;超出範圍的呼叫會拋出例外(Exception)。
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)
}
Page Navigation
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)
}
Observing Page Changes
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)"
}
Text Search and Selection
Synchronous Search
let results: [PDFSelection] = document.findString(
"search term", withOptions: [.caseInsensitive]
)
Asynchronous Search
若要對大型文件進行背景搜尋,請使用 PDFDocumentDelegate。
實作 didMatchString(_:) 可接收每一次的比對結果,實作 documentDidEndDocumentFind(_:) 則可在搜尋完成時收到通知。
Incremental Search and Find Interaction
// 從目前選取的位置開始尋找下一個比對項目
let next = document.findString("term", fromSelection: current, withOptions: [.caseInsensitive])
// 系統搜尋列;請注意 Setup 中的可用性限制
pdfView.isFindInteractionEnabled = true
Text Extraction
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
}
Highlighting Search Results
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)
}
Annotations
標註是透過 PDFAnnotation(bounds:forType:withProperties:) 建立,並新增至 PDFPage 中。
Highlight Annotation
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)
}
Text Note Annotation
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)
Free Text Annotation
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)
Link Annotation
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)
Removing Annotations
for annotation in page.annotations {
page.removeAnnotation(annotation)
}
常見的子類型包括 .highlight、.underline、.strikeOut、.text、.freeText、.ink、.link、.line、.square、.circle、.stamp 以及 .widget。
Thumbnails
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)
Generating Thumbnails Programmatically
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 Integration
在 SwiftUI 中,可將 PDFView 封裝在 UIViewRepresentable 內。專門用來設定 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
}
}
}
Usage
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")
}
}
}
如需包含頁面追蹤、標註點擊偵測(Hit detection)與 Coordinator 模式的互動式封裝範例,請參閱 references/pdfkit-patterns.md。
Page Overlays
PDFPageOverlayViewProvider 可將 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 被強參照(strongly owned)。關於覆蓋層生命週期與儲存處理,請閱讀 references/pdfkit-patterns.md。
Common Mistakes
DON'T: Force-unwrap PDFDocument init
PDFDocument(url:) 與 PDFDocument(data:) 皆為可能失敗的建構子(failable initializers)。
// 錯誤
let document = PDFDocument(url: url)!
// 正確
guard let document = PDFDocument(url: url) else { return }
DON'T: Forget autoScales on PDFView
未設定 autoScales 時,PDF 將以原始解析度渲染。
// 錯誤
pdfView.document = document
// 正確
pdfView.autoScales = true
pdfView.document = document
DON'T: Ignore PDF coordinate system in annotations
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)
DON'T: Modify annotations on a background thread
PDFKit 類別並非執行緒安全(Thread-safe)。
// 錯誤
DispatchQueue.global().async { page.addAnnotation(annotation) }
// 正確
DispatchQueue.main.async { page.addAnnotation(annotation) }
DON'T: Compare PDFDocument with == in UIViewRepresentable
PDFDocument 是參考型別(Reference type)。請使用同一性比較符號(!==)。
// WRONG: Alway
<!-- truncated for translation batch; full body continues in source -->




