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

使用 PDFViewPDFDocumentPDFPagePDFAnnotationPDFSelection 來顯示、導覽、搜尋、標註及操作 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 -->