Swift-ios-skills 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, signatures), extracting text with PDFSelection, navigating pages, generating thumbnails, filling PDF forms, or wrapping PDFView in SwiftUI.
git clone https://github.com/dpearson2699/swift-ios-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dpearson2699/swift-ios-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/pdfkit" ~/.claude/skills/dpearson2699-swift-ios-skills-pdfkit && rm -rf "$T"
skills/pdfkit/SKILL.mdPDFKit
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 |
|---|---|
| One page at a time |
| Pages stacked vertically, scrollable |
| Two pages side by side |
| 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 | | Text markup (yellow highlight) |
| Underline | | Text markup (underline) |
| StrikeOut | | Text markup (strikethrough) |
| Text | | Sticky note icon |
| FreeText | | Inline text block |
| Ink | | Freehand drawing paths |
| Link | | URL or page destination |
| Line | | Straight line with endpoints |
| Square | | Rectangle shape |
| Circle | | Ellipse shape |
| Stamp | | Rubber stamp (Approved, etc.) |
| 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
-
init uses optional binding, not force-unwrapPDFDocument -
set for proper initial displaypdfView.autoScales = true - Page indices checked against
before accesspageCount -
anddisplayMode
configured to match designdisplayDirection - Annotations use PDF coordinate space (origin bottom-left, Y up)
- All PDFKit mutations happen on the main thread
- Password-protected PDFs handled with
/isLockedunlock(withPassword:) - SwiftUI wrapper uses
identity check in!==updateUIView -
notification observed for page trackingPDFViewPageChanged -
linked to the mainPDFThumbnailView.pdfViewPDFView - Large-document search uses async
with delegatebeginFindString - Saved documents use
when encryption neededwrite(to:withOptions:)
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