npx skills add ...
npx skills add dpearson2699/swift-ios-skills --skill pdfkit
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.
npx skills add dpearson2699/swift-ios-skills --skill pdfkit
Display, navigate, search, annotate, and manipulate PDF documents with PDFView, PDFDocument, PDFPage, PDFAnnotation, and PDFSelection.
PDFKit requires no entitlements or Info.plist entries.
| 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+ |
PDFView renders PDF content and handles zoom, scrolling, text selection, and page navigation.
| 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 |
PDFDocument loads from a URL, Data, or can be created empty.
PDFView provides built-in navigation with history tracking.
Use PDFDocumentDelegate for background searches on large documents.
Implement didMatchString(_:) to receive each match and
documentDidEndDocumentFind(_:) for completion.
Annotations are created with PDFAnnotation(bounds:forType:withProperties:)
and added to a PDFPage.
Common subtypes include .highlight, .underline, .strikeOut, .text,
.freeText, .ink, .link, .line, .square, .circle, .stamp, and
.widget.
PDFThumbnailView shows a strip of page thumbnails linked to a PDFView.
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.
For interactive wrappers with page tracking, annotation hit detection, and coordinator patterns, see references/pdfkit-patterns.md.
PDFPageOverlayViewProvider places UIKit views on top of individual pages
for interactive controls or custom rendering beyond standard annotations.
pageOverlayViewProvider is weak, so keep the provider strongly owned. For overlay lifecycle and save handling, read references/pdfkit-patterns.md.
PDFDocument(url:) and PDFDocument(data:) are failable initializers.
Without autoScales, the PDF renders at its native resolution.
PDF page coordinates have origin at the bottom-left with Y increasing upward -- opposite of UIKit.
PDFKit classes are not thread-safe.
PDFDocument is a reference type. Use identity (!==).
PDFDocument init uses optional binding, not force-unwrappdfView.autoScales = true set for proper initial displaypageCount before accessdisplayMode and displayDirection configured to match designisLocked / unlock(withPassword:)!== identity check in updateUIViewPDFViewPageChanged notification observed for page trackingPDFThumbnailView.pdfView linked to the main PDFViewbeginFindString with delegatewrite(to:withOptions:) when encryption neededpdfView.autoScales = true
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.maxScaleFactor = 4.0
pdfView.displaysPageBreaks = true
pdfView.pageShadowsEnabled = true
pdfView.interpolationQuality = .highlet fileDoc = PDFDocument(url: fileURL)
let dataDoc = PDFDocument(data: pdfData)
let emptyDoc = PDFDocument()guard let document = PDFDocument(url: url) else { return }
if document.isLocked {
if !document.unlock(withPassword: userPassword) {
// Show password prompt
}
}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.pageCount
document.insert(PDFPage(), at: count)
if document.pageCount > 2 {
document.removePage(at: 2)
}
if document.pageCount > 3 {
document.exchangePage(at: 0, withPageAt: 3)
}// 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)
}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)"
}let results: [PDFSelection] = document.findString(
"search term", withOptions: [.caseInsensitive]
)// 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 = truelet 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
}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)
}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 = "This is a sticky note."
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 = "Added commentary"
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)
// Internal page link
link.destination = PDFDestination(page: targetPage, at: .zero)for annotation in page.annotations {
page.removeAnnotation(annotation)
}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)
// All pages
let thumbnails = (0..<document.pageCount).compactMap {
document.page(at: $0)?.thumbnail(of: CGSize(width: 120, height: 160), for: .mediaBox)
}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("Unable to load PDF", systemImage: "doc.questionmark")
}
}
}class 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
}
}// WRONG
let document = PDFDocument(url: url)!
// CORRECT
guard let document = PDFDocument(url: url) else { return }// WRONG
pdfView.document = document
// CORRECT
pdfView.autoScales = true
pdfView.document = document// 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)// WRONG
DispatchQueue.global().async { page.addAnnotation(annotation) }
// CORRECT
DispatchQueue.main.async { page.addAnnotation(annotation) }// 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
}
}