pdfkit
PDFKit
Display, navigate, search, annotate, and manipulate PDF documents with
PDFView, PDFDocument, PDFPage, PDFAnnotation, and PDFSelection.
Targets Swift 6.3 / iOS 26+.
Contents
- Setup
- Displaying PDFs
- Loading Documents
- Page Navigation
- Text Search and Selection
- Annotations
- Thumbnails
- SwiftUI Integration
- Common Mistakes
- Review Checklist
- References
Setup
PDFKit requires no entitlements or Info.plist entries.
import PDFKit
Platform availability: iOS 11+, iPadOS 11+, Mac Catalyst 13.1+, macOS 10.4+, tvOS 11+, visionOS 1.0+.
Displaying PDFs
PDFView is a UIView subclass that renders PDF content, handles zoom,
scroll, text selection, and page navigation out of the box.
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
| Mode | Behavior |
|---|---|
.singlePage |
One page at a time |
.singlePageContinuous |
Pages stacked vertically, scrollable |
.twoUp |
Two pages side by side |
.twoUpContinuous |
Two-up with continuous scrolling |
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 loads from a URL, Data, or can be created empty.
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) {
// Show password prompt
}
}
Saving and Page Manipulation
document.write(to: outputURL)
document.write(to: outputURL, withOptions: [
.ownerPasswordOption: "ownerPass", .userPasswordOption: "userPass"
])
let data = document.dataRepresentation()
// Pages (0-based)
let count = document.pageCount
document.insert(PDFPage(), at: count)
document.removePage(at: 2)
document.exchangePage(at: 0, withPageAt: 3)
Page Navigation
PDFView provides built-in navigation with history tracking.
// Go to a specific page
if let page = pdfView.document?.page(at: 5) {
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 a page
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
Use PDFDocumentDelegate for background searches on large documents.
Implement didMatchString(_:) to receive each match and
documentDidEndDocumentFind(_:) for completion.
Incremental Search and Find Interaction
// Find next match from current selection
let next = document.findString("term", fromSelection: current, withOptions: [.caseInsensitive])
// System find bar (iOS 16+)
pdfView.isFindInteractionEnabled = true
Text Extraction
let fullText = document.string // Entire document
let pageText = document.page(at: 0)?.string // Single page
let attributed = document.page(at: 0)?.attributedString // With formatting
// Region-based extraction
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
}
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
Annotations are created with PDFAnnotation(bounds:forType:withProperties:)
and added to a 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)
// Internal page link
link.destination = PDFDestination(page: targetPage, at: .zero)
Removing Annotations
for annotation in page.annotations {
page.removeAnnotation(annotation)
}
Annotation Subtypes Reference
| Subtype | Constant | Purpose |
|---|---|---|
| Highlight | .highlight |
Text markup (yellow highlight) |
| Underline | .underline |
Text markup (underline) |
| StrikeOut | .strikeOut |
Text markup (strikethrough) |
| Text | .text |
Sticky note icon |
| FreeText | .freeText |
Inline text block |
| Ink | .ink |
Freehand drawing paths |
| Link | .link |
URL or page destination |
| Line | .line |
Straight line with endpoints |
| Square | .square |
Rectangle shape |
| Circle | .circle |
Ellipse shape |
| Stamp | .stamp |
Rubber stamp (Approved, etc.) |
| Widget | .widget |
Form element (text field, checkbox) |
Thumbnails
PDFThumbnailView
PDFThumbnailView shows a strip of page thumbnails linked to a 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)
// All pages
let thumbnails = (0..<document.pageCount).compactMap {
document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}
SwiftUI Integration
Wrap PDFView in a UIViewRepresentable for 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
}
}
}
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")
}
}
}
For interactive wrappers with page tracking, annotation hit detection, and coordinator patterns, see references/pdfkit-patterns.md.
Page Overlays (iOS 16+)
PDFPageOverlayViewProvider places UIKit views on top of individual pages
for interactive controls or custom rendering beyond standard annotations.
class OverlayProvider: NSObject, PDFPageOverlayViewProvider {
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
let overlay = UIView()
// Add custom subviews
return overlay
}
}
pdfView.pageOverlayViewProvider = overlayProvider
Common Mistakes
DON'T: Force-unwrap PDFDocument init
PDFDocument(url:) and PDFDocument(data:) are failable initializers.
// WRONG
let document = PDFDocument(url: url)!
// CORRECT
guard let document = PDFDocument(url: url) else { return }
DON'T: Forget autoScales on PDFView
Without autoScales, the PDF renders at its native resolution.
// WRONG
pdfView.document = document
// CORRECT
pdfView.autoScales = true
pdfView.document = document
DON'T: Ignore PDF coordinate system in annotations
PDF page coordinates have origin at the bottom-left with Y increasing upward -- opposite of 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)
DON'T: Modify annotations on a background thread
PDFKit classes are not thread-safe.
// WRONG
DispatchQueue.global().async { page.addAnnotation(annotation) }
// CORRECT
DispatchQueue.main.async { page.addAnnotation(annotation) }
DON'T: Compare PDFDocument with == in UIViewRepresentable
PDFDocument is a reference type. Use identity (!==).
// 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
}
}
Review Checklist
-
PDFDocumentinit uses optional binding, not force-unwrap -
pdfView.autoScales = trueset for proper initial display - Page indices checked against
pageCountbefore access -
displayModeanddisplayDirectionconfigured to match design - Annotations use PDF coordinate space (origin bottom-left, Y up)
- All PDFKit mutations happen on the main thread
- Password-protected PDFs handled with
isLocked/unlock(withPassword:) - SwiftUI wrapper uses
!==identity check inupdateUIView -
PDFViewPageChangednotification observed for page tracking -
PDFThumbnailView.pdfViewlinked to the mainPDFView - Large-document search uses async
beginFindStringwith delegate - Saved documents use
write(to:withOptions:)when encryption needed
References
- Extended patterns (forms, watermarks, merging, printing, overlays, outlines, custom drawing): references/pdfkit-patterns.md
- PDFKit framework
- PDFView
- PDFDocument
- PDFPage
- PDFAnnotation
- PDFSelection
- PDFThumbnailView
- PDFPageOverlayViewProvider
- Adding Widgets to a PDF Document
- Adding Custom Graphics to a PDF