pdfkit

$npx mdskill add dpearson2699/swift-ios-skills/pdfkit

Display and manipulate PDF documents using PDFKit.

  • Embed PDFView to show PDF files in your app.
  • Uses PDFDocument, PDFPage, PDFAnnotation, and PDFSelection APIs.
  • Determines actions based on user input or programmatic requests.
  • Returns rendered PDF, annotations, text selections, or thumbnails.

SKILL.md

.github/skills/pdfkitView on GitHub ↗
---
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`.

## Contents

- [Setup](#setup)
- [Displaying PDFs](#displaying-pdfs)
- [Loading Documents](#loading-documents)
- [Page Navigation](#page-navigation)
- [Text Search and Selection](#text-search-and-selection)
- [Annotations](#annotations)
- [Thumbnails](#thumbnails)
- [SwiftUI Integration](#swiftui-integration)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Setup

PDFKit requires no entitlements or Info.plist entries.

```swift
import 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.

```swift
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

```swift
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.

```swift
let fileDoc = PDFDocument(url: fileURL)
let dataDoc = PDFDocument(data: pdfData)
let emptyDoc = PDFDocument()
```

### Password-Protected PDFs

```swift
guard let document = PDFDocument(url: url) else { return }
if document.isLocked {
    if !document.unlock(withPassword: userPassword) {
        // Show password prompt
    }
}
```

### Saving and Page Manipulation

```swift
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)
}
```

## 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

```swift
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

```swift
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

```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

```swift
let 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

```swift
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

```swift
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

```swift
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

```swift
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

```swift
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

```swift
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`.

```swift
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

```swift
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. 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.

```swift
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

```swift
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](references/pdfkit-patterns.md).

### Page Overlays

`PDFPageOverlayViewProvider` places UIKit views on top of individual pages
for interactive controls or custom rendering beyond standard annotations.

```swift
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
    }
}
```

`pageOverlayViewProvider` is weak, so keep the provider strongly owned. For overlay lifecycle and save handling, read [references/pdfkit-patterns.md](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

- [ ] `PDFDocument` init uses optional binding, not force-unwrap
- [ ] `pdfView.autoScales = true` set for proper initial display
- [ ] Page indices checked against `pageCount` before access
- [ ] `displayMode` and `displayDirection` configured 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 in `updateUIView`
- [ ] `PDFViewPageChanged` notification observed for page tracking
- [ ] `PDFThumbnailView.pdfView` linked to the main `PDFView`
- [ ] Large-document search uses async `beginFindString` with 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](references/pdfkit-patterns.md)
- [PDFKit framework](https://sosumi.ai/documentation/pdfkit)
- [PDFView](https://sosumi.ai/documentation/pdfkit/pdfview)
- [PDFDocument](https://sosumi.ai/documentation/pdfkit/pdfdocument)
- [PDFPage](https://sosumi.ai/documentation/pdfkit/pdfpage), [PDFAnnotation](https://sosumi.ai/documentation/pdfkit/pdfannotation), [PDFSelection](https://sosumi.ai/documentation/pdfkit/pdfselection), [PDFThumbnailView](https://sosumi.ai/documentation/pdfkit/pdfthumbnailview)
- [PDFPageOverlayViewProvider](https://sosumi.ai/documentation/pdfkit/pdfpageoverlayviewprovider)
- [Adding Widgets to a PDF Document](https://sosumi.ai/documentation/pdfkit/adding-widgets-to-a-pdf-document)
- [Adding Custom Graphics to a PDF](https://sosumi.ai/documentation/pdfkit/adding-custom-graphics-to-a-pdf)

More from dpearson2699/swift-ios-skills

SkillDescription
accessorysetupkitDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
activitykitImplement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
adattributionkitMeasure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
alarmkitImplement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, system Stop and AlarmButton secondary actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
app-clipsBuild iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
app-intentsImplement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
app-store-reviewAudits App Store submission readiness and rejection risk across current review guidelines, PrivacyInfo.xcprivacy and required-reason APIs, privacy labels, ATT, StoreKit payments, metadata, entitlements, widgets, and Live Activities. Use when preparing a submission, responding to rejection, reconciling privacy evidence, or separating upload blockers from cleanup.
apple-on-device-aiBuild private, on-device AI features on iPhone, iPad, and Mac with Foundation Models, Core ML, MLX Swift, or llama.cpp. Use when choosing an Apple-local model runtime, building an Apple Intelligence chatbot or tool-calling feature, running an LLM on Apple Silicon, converting or compressing a Python model for Core ML, or comparing on-device inference backends. For Swift Core ML loading and prediction code, use the coreml skill.
appmigrationkitTransfer app data to or from other platforms using AppMigrationKit. Use when implementing system-orchestrated one-time migration between iOS and Android or another platform, building an AppMigrationExtension, packaging transportable resources with ResourcesArchiver, importing resources on the destination device, reporting import progress, handling migration errors and app group cleanup, checking MigrationStatus, or testing migration code with AppMigrationTester.
audioaccessorykitSupport automatic audio switching for paired third-party Bluetooth headphones or earbuds with AudioAccessoryKit. Use when a companion app registers an audio accessory, an app extension reports worn/removed placement or connected source-device changes, or AccessoryControlDevice capabilities and errors need handling. Do not use for general AVAudioSession routing, Bluetooth transport, or initial accessory pairing.