PDFKit
Display, navigate, search, annotate, and manipulate PDF documents with PDFView, PDFDocument, PDFPage, PDFAnnotation, and PDFSelection.
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.
swiftimport PDFKit
| API | Availability |
|---|---|
| PDFKit framework | iOS/iPadOS/tvOS 11+, Mac Catalyst 13.1+, macOS 10.4+, visionOS 1.0+ |
| Find interaction and page overlays | iOS/iPadOS 16+ |
Displaying PDFs
PDFView renders PDF content and handles zoom, scrolling, text selection, and page navigation.
swiftimport 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
swiftpdfView.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.
swiftlet fileDoc = PDFDocument(url: fileURL) let dataDoc = PDFDocument(data: pdfData) let emptyDoc = PDFDocument()
Password-Protected PDFs
swiftguard let document = PDFDocument(url: url) else { return } if document.isLocked { if !document.unlock(withPassword: userPassword) { // Show password prompt } }
Saving and Page Manipulation
swiftdocument.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) }
Page Navigation
PDFView provides built-in navigation with history tracking.
swift// 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) }
Observing Page Changes
swiftNotificationCenter.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
swiftlet 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
swift// 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
Text Extraction
swiftlet 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 }
Highlighting Search Results
swiftlet 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
swiftfunc 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
swiftlet 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
swiftlet 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
swiftlet 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
swiftfor annotation in page.annotations { page.removeAnnotation(annotation) }
Common subtypes include .highlight, .underline, .strikeOut, .text,
.freeText, .ink, .link, .line, .square, .circle, .stamp, and
.widget.
Thumbnails
PDFThumbnailView
PDFThumbnailView shows a strip of page thumbnails linked to a PDFView.
swiftlet thumbnailView = PDFThumbnailView() thumbnailView.pdfView = pdfView thumbnailView.thumbnailSize = CGSize(width: 60, height: 80) thumbnailView.layoutMode = .vertical thumbnailView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(thumbnailView)
Generating Thumbnails Programmatically
swiftlet 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. PDF-specific wrappers
that configure PDFView, pages, annotations, search, thumbnails, or overlays
belong in this skill; route only generic representable lifecycle, layout, or SwiftUI state architecture questions to SwiftUI/UIKit interop guidance.
swiftimport 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
swiftstruct 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
PDFPageOverlayViewProvider places UIKit views on top of individual pages
for interactive controls or custom rendering beyond standard annotations.
swiftclass 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 is weak, so keep the provider strongly owned. For overlay lifecycle and save handling, read references/pdfkit-patterns.md.
Common Mistakes
DON'T: Force-unwrap PDFDocument init
PDFDocument(url:) and PDFDocument(data:) are failable initializers.
swift// 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.
swift// 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.
swift// 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.
swift// 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 (!==).
swift// 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


