pdfkit

pdfkit

熱門

使用 PDFKit 顯示與操作 PDF 文件。適用於在 app 中嵌入 PDFView 展示 PDF 檔案、建立或修改 PDFDocument 實例、新增標註(螢光筆高亮標示、筆記、簽名元件)、透過 PDFSelection 擷取文字、頁面導覽、產生縮圖、填寫 PDF 表單,或在 SwiftUI 中封裝 PDFView 等情境。

967星標
49分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
pdfkit
描述

使用 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 -->