mcpField
P
pdfkit

pdfkit

github.com/dpearson2699ドキュメント1.2k3.2k 回インストール13 週間前に更新
リポジトリ

pdfkit について

Appleプラットフォームの開発者向けに、PDFKitでPDFの表示や編集機能を実装する方法をまとめたガイドです。検索、注釈、サムネイル、SwiftUI連携と実装上の注意点を扱います。

主な機能

  1. 01PDFの表示、拡大縮小、移動
  2. 02文書内の検索とテキスト抽出
  3. 03PDF注釈の作成と管理
  4. 04ページサムネイルの生成
  5. 05PDFViewとSwiftUIの連携

活用シーン

  1. 01アプリ内で移動可能なPDFを表示
  2. 02大きなPDFを検索して結果を処理
  3. 03PDFページにハイライトやテキスト注釈を追加

インストール

コマンドをコピーしてターミナルで実行します。

$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一度に 1 ページを表示
.singlePageContinuousページを縦に並べてスクロール可能にします
.twoUp2 ページを横に並べて表示
.twoUpContinuous2 ページ表示で連続スクロールします
スケーリングと外観
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は weak 参照のため、プロバイダーを強参照で保持してください。オーバーレイのライフサイクルと保存処理については、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 の変更はすべてメインスレッドで行う
  • パスワードで保護された PDF は isLocked / unlock(withPassword:) で処理する
  • SwiftUI ラッパーの updateUIView では !== によるアイデンティティチェックを使う
  • ページ追跡のために PDFViewPageChanged 通知を監視する
  • PDFThumbnailView.pdfView をメインの PDFView に関連付ける
  • 大きなドキュメントの検索では、デリゲートを使って非同期の beginFindString を使用する
  • 暗号化が必要な場合は、保存するドキュメントに write(to:withOptions:) を使う

参考資料

コメント