PDFKit
Affichez, naviguez, recherchez, annotez et manipulez des documents PDF avec
PDFView, PDFDocument, PDFPage, PDFAnnotation et PDFSelection.
Cible Swift 6.3 / iOS 26+.
Table des matières
- Configuration
- Affichage des PDF
- Chargement de documents
- Navigation entre les pages
- Recherche et sélection de texte
- Annotations
- Vignettes
- Intégration SwiftUI
- Erreurs courantes
- Checklist de révision
- Références
Configuration
PDFKit ne nécessite ni entitlements ni entrées Info.plist.
import PDFKit
Disponibilité des plateformes : iOS 11+, iPadOS 11+, Mac Catalyst 13.1+, macOS 10.4+, tvOS 11+, visionOS 1.0+.
Affichage des PDF
PDFView est une sous-classe de UIView qui affiche le contenu PDF, gère le zoom,
le défilement, la sélection de texte et la navigation entre les pages prêts à l'emploi.
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)
}
}
}
Modes d'affichage
| Mode | Comportement |
|---|---|
.singlePage | Une seule page à la fois |
.singlePageContinuous | Pages empilées verticalement, défilables |
.twoUp | Deux pages côte à côte |
.twoUpContinuous | Deux pages avec défilement continu |
Mise à l'échelle et apparence
pdfView.autoScales = true
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0
pdfView.displaysPageBreaks = true
pdfView.pageShadowsEnabled = true
pdfView.interpolationQuality = .high
Chargement de documents
PDFDocument se charge à partir d'une URL, de Data, ou peut être créé vide.
let fileDoc = PDFDocument(url: fileURL)
let dataDoc = PDFDocument(data: pdfData)
let emptyDoc = PDFDocument()
PDF protégés par mot de passe
guard let document = PDFDocument(url: url) else { return }
if document.isLocked {
if !document.unlock(withPassword: userPassword) {
// Afficher la demande de mot de passe
}
}
Sauvegarde et manipulation de pages
document.write(to: outputURL)
document.write(to: outputURL, withOptions: [
.ownerPasswordOption: "ownerPass", .userPasswordOption: "userPass"
])
let data = document.dataRepresentation()
// Pages (indexées à partir de 0)
let count = document.pageCount
document.insert(PDFPage(), at: count)
document.removePage(at: 2)
document.exchangePage(at: 0, withPageAt: 3)
Navigation entre les pages
PDFView fournit une navigation intégrée avec suivi de l'historique.
// Aller à une page spécifique
if let page = pdfView.document?.page(at: 5) {
pdfView.go(to: page)
}
// Navigation séquentielle
pdfView.goToNextPage(nil)
pdfView.goToPreviousPage(nil)
pdfView.goToFirstPage(nil)
pdfView.goToLastPage(nil)
// Vérifier l'état de la navigation
if pdfView.canGoToNextPage { /* ... */ }
// Navigation dans l'historique
if pdfView.canGoBack { pdfView.goBack(nil) }
// Aller à un point spécifique sur une page
let destination = PDFDestination(page: page, at: CGPoint(x: 0, y: 500))
pdfView.go(to: destination)
Observation des changements de page
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) sur \(doc.pageCount)"
}
Recherche et sélection de texte
Recherche synchrone
let results: [PDFSelection] = document.findString(
"terme de recherche", withOptions: [.caseInsensitive]
)
Recherche asynchrone
Utilisez PDFDocumentDelegate pour les recherches en arrière-plan sur de grands documents.
Implémentez didMatchString(_:) pour recevoir chaque correspondance et
documentDidEndDocumentFind(_:) pour la fin.
Recherche incrémentale et interaction de recherche
// Correspondance suivante à partir de la sélection courante
let next = document.findString("terme", fromSelection: current, withOptions: [.caseInsensitive])
// Barre de recherche système (iOS 16+)
pdfView.isFindInteractionEnabled = true
Extraction de texte
let fullText = document.string // Document entier
let pageText = document.page(at: 0)?.string // Page unique
let attributed = document.page(at: 0)?.attributedString // Avec mise en forme
// Extraction par région
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
}
Mise en surbrillance des résultats de recherche
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
Les annotations sont créées avec PDFAnnotation(bounds:forType:withProperties:)
et ajoutées à une PDFPage.
Annotation de surlignement
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)
}
Annotation de note texte
let note = PDFAnnotation(
bounds: CGRect(x: 100, y: 700, width: 30, height: 30),
forType: .text, withProperties: nil
)
note.contents = "Ceci est une note adhésive."
note.color = .systemYellow
note.iconType = .comment
page.addAnnotation(note)
Annotation de texte libre
let freeText = PDFAnnotation(
bounds: CGRect(x: 50, y: 600, width: 300, height: 40),
forType: .freeText, withProperties: nil
)
freeText.contents = "Commentaire ajouté"
freeText.font = UIFont.systemFont(ofSize: 14)
freeText.fontColor = .darkGray
page.addAnnotation(freeText)
Annotation de lien
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)
// Lien vers une page interne
link.destination = PDFDestination(page: targetPage, at: .zero)
Suppression des annotations
for annotation in page.annotations {
page.removeAnnotation(annotation)
}
Référence des sous-types d'annotations
| Sous-type | Constante | Objectif |
|---|---|---|
| Surlignement | .highlight | Balisage de texte (surlignement jaune) |
| Soulignement | .underline | Balisage de texte (soulignement) |
| Barré | .strikeOut | Balisage de texte (barré) |
| Texte | .text | Icône de note adhésive |
| Texte libre | .freeText | Bloc de texte en ligne |
| Encre | .ink | Trajets de dessin à main levée |
| Lien | .link | URL ou destination de page |
| Ligne | .line | Ligne droite avec extrémités |
| Carré | .square | Forme rectangulaire |
| Cercle | .circle | Forme elliptique |
| Tampon | .stamp | Tampon en caoutchouc (Approuvé, etc.) |
| Widget | .widget | Élément de formulaire (champ de texte, case à cocher) |
Vignettes
PDFThumbnailView
PDFThumbnailView affiche une bande de vignettes de pages liée à une PDFView.
let thumbnailView = PDFThumbnailView()
thumbnailView.pdfView = pdfView
thumbnailView.thumbnailSize = CGSize(width: 60, height: 80)
thumbnailView.layoutMode = .vertical
thumbnailView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(thumbnailView)
Génération programmatique de vignettes
let thumbnail = page.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
// Toutes les pages
let thumbnails = (0..<document.pageCount).compactMap {
document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}
Intégration SwiftUI
Encapsulez PDFView dans un UIViewRepresentable pour SwiftUI.
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
}
}
}
Utilisation
struct DocumentScreen: View {
let url: URL
var body: some View {
if let document = PDFDocument(url: url) {
PDFKitView(document: document)
.ignoresSafeArea()
} else {
ContentUnavailableView("Impossible de charger le PDF", systemImage: "doc.questionmark")
}
}
}
Pour des wrappers interactifs avec suivi de page, détection de tap sur les annotations et patterns de coordinateur, voir references/pdfkit-patterns.md.
Superpositions de pages (iOS 16+)
PDFPageOverlayViewProvider place des vues UIKit au-dessus de pages individuelles
pour des contrôles interactifs ou un rendu personnalisé au-delà des annotations standard.
class OverlayProvider: NSObject, PDFPageOverlayViewProvider {
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let overlay = UIView()
// Ajouter des sous-vues personnalisées
return overlay
}
}
pdfView.pageOverlayViewProvider = overlayProvider
Erreurs courantes
À NE PAS FAIRE : Forcer le déballage de l'initialiseur PDFDocument
PDFDocument(url:) et PDFDocument(data:) sont des initialiseurs failable.
// FAUX
let document = PDFDocument(url: url)!
// CORRECT
guard let document = PDFDocument(url: url) else { return }
À NE PAS FAIRE : Oublier autoScales sur PDFView
Sans autoScales, le PDF s'affiche à sa résolution native.
// FAUX
pdfView.document = document
// CORRECT
pdfView.autoScales = true
pdfView.document = document
À NE PAS FAIRE : Ignorer le système de coordonnées PDF dans les annotations
Les coordonnées des pages PDF ont l'origine en bas à gauche, avec Y croissant vers le haut — à l'inverse d'UIKit.
// FAUX : Coordonnées UIKit
let bounds = CGRect(x: 50, y: 50, width: 200, height: 30)
// CORRECT : Coordonnées PDF (origine en bas à gauche)
let pageBounds = page.bounds(for: .mediaBox)
let pdfY = pageBounds.height - 50 - 30
let bounds = CGRect(x: 50, y: pdfY, width: 200, height: 30)
À NE PAS FAIRE : Modifier les annotations sur un thread d'arrière-plan
Les classes PDFKit ne sont pas thread-safe.
// FAUX
DispatchQueue.global().async { page.addAnnotation(annotation) }
// CORRECT
DispatchQueue.main.async { page.addAnnotation(annotation) }
À NE PAS FAIRE : Comparer PDFDocument avec == dans UIViewRepresentable
PDFDocument est un type de référence. Utilisez l'identité (!==).
// FAUX : Remplace toujours le document
func updateUIView(_ pdfView: PDFView, context: Context) {
pdfView.document = document
}
// CORRECT
func updateUIView(_ pdfView: PDFView, context: Context) {
if pdfView.document !== document {
pdfView.document = document
}
}
Checklist de révision
- L'initialiseur de
PDFDocumentutilise une liaison optionnelle, pas un force-unwrap -
pdfView.autoScales = truedéfini pour un affichage initial correct - Les index de pages sont vérifiés par rapport à
pageCountavant l'accès -
displayModeetdisplayDirectionconfigurés pour correspondre au design - Les annotations utilisent l'espace de coordonnées PDF (origine en bas à gauche, Y vers le haut)
- Toutes les mutations de PDFKit se font sur le thread principal
- Les PDF protégés par mot de passe sont gérés avec
isLocked/unlock(withPassword:) - Le wrapper SwiftUI utilise une vérification d'identité
!==dansupdateUIView - La notification
PDFViewPageChangedest observée pour le suivi de page -
PDFThumbnailView.pdfViewest lié à laPDFViewprincipale - La recherche sur de grands documents utilise
beginFindStringasynchrone avec délégué - Les documents sauvegardés utilisent
write(to:withOptions:)en cas de chiffrement nécessaire
Références
- Patterns avancés (formulaires, filigranes, fusion, impression, superpositions, contours, dessin personnalisé) : references/pdfkit-patterns.md
- Framework PDFKit
- PDFView
- PDFDocument
- PDFPage
- PDFAnnotation
- PDFSelection
- PDFThumbnailView
- PDFPageOverlayViewProvider
- Ajout de widgets à un document PDF
- Ajout de graphiques personnalisés à un PDF