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 | 一度に1ページ |
.singlePageContinuous | ページを縦に積み重ね、スクロール可能 |
.twoUp | 2ページを横に並べて表示 |
.twoUpContinuous | 2ページ表示で連続スクロール |
拡大縮小と外観
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
// パスワードで保護された PDF の処理
if let document = PDFDocument(url: url), document.isLocked {
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 | テキストマークアップ(黄色ハイライト) |
| 下線 | .underline | テキストマークアップ(下線) |
| 取り消し線 | .strikeOut | テキストマークアップ(取り消し線) |
| テキスト | .text | 付箋アイコン |
| 自由テキスト | .freeText | インラインテキストブロック |
| インク | .ink | フリーハンド描画パス |
| リンク | .link | URL またはページ移動先 |
| 線 | .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)
// 全ページ
let thumbnails = (0..<document.pageCount).compactMap {
document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}
SwiftUI 統合
SwiftUI で使用するには PDFView を UIViewRepresentable でラップします。
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)!
// 正しい
if let document = PDFDocument(url: url) {
// 文書を使用
}
間違い: 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 の変更はメインスレッドで行われている
- パスワード保護された PDF は
isLocked/unlock(withPassword:)で処理している - SwiftUI ラッパーは
updateUIViewで!==による同一性チェックを使用している -
PDFViewPageChanged通知をページ追跡のために監視している -
PDFThumbnailView.pdfViewがメインのPDFViewにリンクされている - 大きな文書の検索には、デリゲートを使った非同期
beginFindStringを使用している - 保存された文書は、暗号化が必要な場合に
write(to:withOptions:)を使用している
参照
- 拡張パターン(フォーム、透かし、マージ、印刷、オーバーレイ、アウトライン、カスタム描画): references/pdfkit-patterns.md
- PDFKit フレームワーク
- PDFView
- PDFDocument
- PDFPage
- PDFAnnotation
- PDFSelection
- PDFThumbnailView
- PDFPageOverlayViewProvider
- PDF 文書へのウィジェット追加
- PDF へのカスタムグラフィックス追加