Skill v1.0.4
currentAutomated scan100/1003 files
version: "1.0.4" name: pdfkit description: "Display and manipulate PDF documents using PDFKit. Use when embedding PDFView to show PDF files, creating or modifying PDFDocument instances, adding annotations (highlights, notes, signature widgets), extracting text with PDFSelection, navigating pages, generating thumbnails, filling PDF forms, or wrapping PDFView in SwiftUI."
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 PDFKitimport UIKitclass PDFViewController: UIViewController {let pdfView = PDFView()override func viewDidLoad() {super.viewDidLoad()pdfView.frame = view.boundspdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]view.addSubview(pdfView)pdfView.autoScales = truepdfView.displayMode = .singlePageContinuouspdfView.displayDirection = .verticalif 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 = truepdfView.minScaleFactor = pdfView.scaleFactorForSizeToFitpdfView.maxScaleFactor = 4.0pdfView.displaysPageBreaks = truepdfView.pageShadowsEnabled = truepdfView.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 are zero-based. Validate indices; out-of-range calls raise exceptions.let count = document.pageCountdocument.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.
// Go to a specific pagelet pageIndex = 5if let document = pdfView.document,pageIndex >= 0,pageIndex < document.pageCount,let page = document.page(at: pageIndex) {pdfView.go(to: page)}// Sequential navigationpdfView.goToNextPage(nil)pdfView.goToPreviousPage(nil)pdfView.goToFirstPage(nil)pdfView.goToLastPage(nil)// Check navigation stateif pdfView.canGoToNextPage { /* ... */ }// History navigationif pdfView.canGoBack { pdfView.goBack(nil) }// Go to a specific point on the current pageif let page = pdfView.currentPage {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 selectionlet next = document.findString("term", fromSelection: current, withOptions: [.caseInsensitive])// System find bar (iOS 16+)pdfView.isFindInteractionEnabled = true
Text Extraction
let fullText = document.string // Entire documentlet firstPage = document.pageCount > 0 ? document.page(at: 0) : nillet pageText = firstPage?.string // Single pagelet attributed = firstPage?.attributedString // With formatting// Region-based extractionif let page = firstPage {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 = resultsif 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 = .systemYellownote.iconType = .commentpage.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 = .darkGraypage.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 linklink.destination = PDFDestination(page: targetPage, at: .zero)
Removing Annotations
for 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.
let thumbnailView = PDFThumbnailView()thumbnailView.pdfView = pdfViewthumbnailView.thumbnailSize = CGSize(width: 60, height: 80)thumbnailView.layoutMode = .verticalthumbnailView.translatesAutoresizingMaskIntoConstraints = falseview.addSubview(thumbnailView)
Generating Thumbnails Programmatically
let thumbnail = page.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)// All pageslet 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.
import SwiftUIimport PDFKitstruct PDFKitView: UIViewRepresentable {let document: PDFDocumentfunc makeUIView(context: Context) -> PDFView {let pdfView = PDFView()pdfView.autoScales = truepdfView.displayMode = .singlePageContinuouspdfView.document = documentreturn pdfView}func updateUIView(_ pdfView: PDFView, context: Context) {if pdfView.document !== document {pdfView.document = document}}}
Usage
struct DocumentScreen: View {let url: URLvar 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 subviewsreturn 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.
// WRONGlet document = PDFDocument(url: url)!// CORRECTguard let document = PDFDocument(url: url) else { return }
DON'T: Forget autoScales on PDFView
Without autoScales, the PDF renders at its native resolution.
// WRONGpdfView.document = document// CORRECTpdfView.autoScales = truepdfView.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 coordinateslet 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 - 30let bounds = CGRect(x: 50, y: pdfY, width: 200, height: 30)
DON'T: Modify annotations on a background thread
PDFKit classes are not thread-safe.
// WRONGDispatchQueue.global().async { page.addAnnotation(annotation) }// CORRECTDispatchQueue.main.async { page.addAnnotation(annotation) }
DON'T: Compare PDFDocument with == in UIViewRepresentable
PDFDocument is a reference type. Use identity (!==).
// WRONG: Always replaces documentfunc updateUIView(_ pdfView: PDFView, context: Context) {pdfView.document = document}// CORRECTfunc 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