---
name: pdfkit
description: "PDFKit on iOS: PDFView display and SwiftUI wrapping, creating and editing PDFDocument and PDFPage, highlight, note, signature and form annotations, PDFSelection search and text extraction, page navigation, thumbnails, form filling, encrypted saving. Use when an app displays, searches, annotates, fills, builds or saves PDFs."
metadata:
  source: multi-agent-pipeline
---

# PDFKit

PDFKit renders, searches and edits PDF files. Most work touches five types:
`PDFView` (the on-screen viewer), `PDFDocument` (the file), `PDFPage` (one
page), `PDFAnnotation` (marks and form fields on a page) and `PDFSelection`
(a run of text). Examples target Swift 6.3 and iOS 26.

Freehand drawing tools are not PDFKit work: use PencilKit or PaperKit.

## Setup

No entitlement and no Info.plist key is needed.

```swift
import PDFKit
```

| Platform | Minimum |
|---|---|
| iOS, iPadOS | 11 |
| Mac Catalyst | 13.1 |
| macOS | 10.4 |
| tvOS | 11 |
| visionOS | 1.0 |

## Showing a PDF

`PDFView` is a `UIView` subclass. Out of the box it zooms, scrolls, selects text
and pages through the document.

```swift
import PDFKit
import UIKit

final class ManualViewController: UIViewController {
    private let reader = PDFView()

    override func viewDidLoad() {
        super.viewDidLoad()
        reader.frame = view.bounds
        reader.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        reader.autoScales = true
        reader.displayMode = .singlePageContinuous
        reader.displayDirection = .vertical
        view.addSubview(reader)

        if let url = Bundle.main.url(forResource: "assembly-guide", withExtension: "pdf"),
           let file = PDFDocument(url: url) {
            reader.document = file
        }
    }
}
```

### Display modes

| Mode | Layout |
|---|---|
| `.singlePage` | one page on screen at a time |
| `.singlePageContinuous` | one column of pages, scrolling |
| `.twoUp` | facing pages side by side |
| `.twoUpContinuous` | facing pages, scrolling |

### Scale and look

```swift
import PDFKit

@MainActor
func tune(_ reader: PDFView) {
    reader.autoScales = true
    reader.minScaleFactor = reader.scaleFactorForSizeToFit
    reader.maxScaleFactor = 5.0
    reader.displaysPageBreaks = true
    reader.pageShadowsEnabled = false
    reader.interpolationQuality = .high
}
```

## Loading documents

There are three initializers: `PDFDocument(url:)`, `PDFDocument(data:)` and
the empty `PDFDocument()`. The first two are failable.

### Locked files

```swift
import PDFKit

func open(_ file: PDFDocument, passcode: String) -> Bool {
    guard file.isLocked else { return true }
    return file.unlock(withPassword: passcode)
}
```

If `unlock(withPassword:)` returns `false`, ask the user for the password again.

### Saving and rearranging pages

```swift
import PDFKit

func export(_ file: PDFDocument, to url: URL, ownerKey: String, readerKey: String) -> Bool {
    file.write(to: url, withOptions: [
        .ownerPasswordOption: ownerKey,
        .userPasswordOption: readerKey
    ])
}

func reorder(_ file: PDFDocument) {
    let blank = PDFPage()
    file.insert(blank, at: file.pageCount)
    if file.pageCount > 2 {
        file.exchangePage(at: 0, withPageAt: 2)
    }
    if file.pageCount > 1 {
        file.removePage(at: 1)
    }
}
```

- `write(to:)` saves as is; `write(to:withOptions:)` with
  `.ownerPasswordOption` and `.userPasswordOption` encrypts.
- `dataRepresentation()` returns `Data?` for upload or sharing.
- Page indices start at 0. Inserting at `pageCount` appends.
- An index outside `0..<pageCount` raises an Objective-C exception rather than
  returning `nil`, so check it before `page(at:)`, `insert(_:at:)`,
  `removePage(at:)` or `exchangePage(at:withPageAt:)`.

## Navigating

`PDFView` keeps its own history of where the user has been.

```swift
import PDFKit

@MainActor
func show(pageNumber index: Int, in reader: PDFView) {
    guard let file = reader.document, index >= 0, index < file.pageCount,
          let target = file.page(at: index) else { return }
    reader.go(to: target)
}

@MainActor
func stepThrough(_ reader: PDFView) {
    if reader.canGoToNextPage { reader.goToNextPage(nil) }
    reader.goToPreviousPage(nil)
    reader.goToFirstPage(nil)
    reader.goToLastPage(nil)
    if reader.canGoBack { reader.goBack(nil) }
}

@MainActor
func jump(to point: CGPoint, on page: PDFPage, in reader: PDFView) {
    reader.go(to: PDFDestination(page: page, at: point))
    _ = reader.currentPage
}
```

### Tracking the current page

```swift
import PDFKit

@MainActor
func watchPages(of reader: PDFView, update: @escaping @MainActor @Sendable (String) -> Void) -> NSObjectProtocol {
    NotificationCenter.default.addObserver(forName: .PDFViewPageChanged, object: reader, queue: .main) { _ in
        MainActor.assumeIsolated {
            guard let file = reader.document, let page = reader.currentPage else { return }
            update("\(file.index(for: page) + 1) / \(file.pageCount)")
        }
    }
}
```

## Search and text

### Synchronous

`findString(_:withOptions:)` returns every match as `[PDFSelection]`:

```swift
import PDFKit

func matches(of term: String, in file: PDFDocument) -> [PDFSelection] {
    file.findString(term, withOptions: [.caseInsensitive])
}
```

### Asynchronous

For long documents, search in the background with `beginFindString(_:withOptions:)`
and a `PDFDocumentDelegate`. `didMatchString(_:)` is called per hit and
`documentDidEndDocumentFind(_:)` when the pass finishes.

```swift
import PDFKit

final class ContractSearch: NSObject, PDFDocumentDelegate {
    private(set) var hits: [PDFSelection] = []
    var onFinish: (([PDFSelection]) -> Void)?

    func run(_ term: String, in file: PDFDocument) {
        hits.removeAll()
        file.delegate = self
        file.beginFindString(term, withOptions: [.caseInsensitive])
    }

    func didMatchString(_ instance: PDFSelection) {
        hits.append(instance)
    }

    func documentDidEndDocumentFind(_ notification: Notification) {
        onFinish?(hits)
    }
}
```

### Next match and the system find bar

- `findString(_:fromSelection:withOptions:)` returns the first hit after a
  given selection, which is how "Find next" works.
- `reader.isFindInteractionEnabled = true` turns on the system find bar
  (iOS 16+).

### Extracting text

```swift
import PDFKit

func textSamples(from file: PDFDocument) -> (all: String?, first: String?, box: String?) {
    let all = file.string
    guard file.pageCount > 0, let first = file.page(at: 0) else { return (all, nil, nil) }
    _ = first.attributedString
    let box = first.selection(for: CGRect(x: 36, y: 700, width: 300, height: 60))?.string
    return (all, first.string, box)
}
```

`document.string` is the whole document, `page.string` one page,
`page.attributedString` keeps fonts and styles, and `page.selection(for:)`
limits extraction to a rectangle. Check `pageCount > 0` before asking for
page 0.

### Highlighting hits

```swift
import PDFKit

@MainActor
func mark(_ hits: [PDFSelection], in reader: PDFView) {
    for hit in hits { hit.color = .systemOrange }
    reader.highlightedSelections = hits
    if let firstHit = hits.first {
        reader.setCurrentSelection(firstHit, animate: true)
        reader.go(to: firstHit)
    }
}
```

## Annotations

Create an annotation with `PDFAnnotation(bounds:forType:withProperties:)` and
attach it with `page.addAnnotation(_:)`.

```swift
import PDFKit
import UIKit

func annotate(_ page: PDFPage, hit: PDFSelection, target: PDFPage) {
    let marker = PDFAnnotation(bounds: hit.bounds(for: page), forType: .highlight, withProperties: nil)
    marker.color = UIColor.systemGreen.withAlphaComponent(0.5)
    page.addAnnotation(marker)

    let note = PDFAnnotation(bounds: CGRect(x: 40, y: 40, width: 24, height: 24), forType: .text, withProperties: nil)
    note.contents = "Check this total"
    note.color = .systemBlue
    note.iconType = .comment
    page.addAnnotation(note)

    let label = PDFAnnotation(bounds: CGRect(x: 72, y: 120, width: 220, height: 30), forType: .freeText, withProperties: nil)
    label.contents = "Approved"
    label.font = .boldSystemFont(ofSize: 16)
    label.fontColor = .systemRed
    page.addAnnotation(label)

    let web = PDFAnnotation(bounds: CGRect(x: 72, y: 170, width: 160, height: 20), forType: .link, withProperties: nil)
    web.url = URL(string: "https://example.org/terms")
    page.addAnnotation(web)

    let internalJump = PDFAnnotation(bounds: CGRect(x: 72, y: 200, width: 160, height: 20), forType: .link, withProperties: nil)
    internalJump.destination = PDFDestination(page: target, at: CGPoint(x: 0, y: 792))
    page.addAnnotation(internalJump)
}

func stripMarkup(from page: PDFPage) {
    for item in page.annotations {
        page.removeAnnotation(item)
    }
}
```

Commonly used subtypes: `.highlight`, `.underline`, `.strikeOut`, `.text`,
`.freeText`, `.ink`, `.link`, `.line`, `.square`, `.circle`, `.stamp`,
`.widget`.

## Thumbnails

### PDFThumbnailView

`PDFThumbnailView` shows a strip of page previews. Point its `pdfView` at the
main viewer so taps navigate and the current page is highlighted.

```swift
import PDFKit

@MainActor
func makeStrip(for reader: PDFView) -> PDFThumbnailView {
    let strip = PDFThumbnailView()
    strip.pdfView = reader
    strip.thumbnailSize = CGSize(width: 48, height: 64)
    strip.layoutMode = .vertical
    return strip
}
```

### Generating images

```swift
import PDFKit
import UIKit

func previews(of file: PDFDocument) -> [UIImage] {
    let size = CGSize(width: 120, height: 160)
    let cover = file.page(at: 0)?.thumbnail(of: size, for: .mediaBox)
    _ = cover
    return (0..<file.pageCount).compactMap { file.page(at: $0)?.thumbnail(of: size, for: .mediaBox) }
}
```

## SwiftUI

Wrap `PDFView` in a `UIViewRepresentable`. Wrappers that configure the viewer,
move between pages, manage annotations, search, show thumbnails or install
overlays belong to this skill. Only generic representable questions (lifecycle,
sizing, state architecture) go to `swiftui-uikit-interop`.

```swift
import PDFKit
import SwiftUI

struct PDFReader: UIViewRepresentable {
    let document: PDFDocument

    func makeUIView(context: Context) -> PDFView {
        let reader = PDFView()
        reader.autoScales = true
        reader.displayMode = .singlePageContinuous
        reader.document = document
        return reader
    }

    func updateUIView(_ reader: PDFView, context: Context) {
        if reader.document !== document {
            reader.document = document
        }
    }
}

struct InvoiceScreen: View {
    let fileURL: URL

    var body: some View {
        if let file = PDFDocument(url: fileURL) {
            PDFReader(document: file)
                .ignoresSafeArea()
        } else {
            ContentUnavailableView("Cannot open invoice", systemImage: "doc.questionmark")
        }
    }
}
```

A version that binds the current page, reports annotation taps and uses a
coordinator is in [the patterns reference](references/pdfkit-patterns.md#coordinator-based-swiftui-wrapper).

### Page overlays (iOS 16+)

`PDFPageOverlayViewProvider` places a UIKit view above each page, for
interactive controls or custom drawing. Implement
`pdfView(_:overlayViewFor:) -> UIView?` and assign the provider to
`pdfView.pageOverlayViewProvider`. That property is weak, so something else
must keep the provider alive.

```swift
import PDFKit
import UIKit

@MainActor
final class StampOverlays: NSObject, @preconcurrency PDFPageOverlayViewProvider {
    func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
        let badge = UILabel()
        badge.text = "DRAFT"
        badge.textColor = .systemRed
        badge.sizeToFit()
        return badge
    }
}

final class DraftViewer: UIViewController {
    private let reader = PDFView()
    private let overlays = StampOverlays()

    override func viewDidLoad() {
        super.viewDidLoad()
        reader.pageOverlayViewProvider = overlays
    }
}
```

## Common mistakes

- **Force-unwrapping the initializers.** `PDFDocument(url:)` and
  `PDFDocument(data:)` return `nil` for a bad file. Use `guard let`.
- **Forgetting `autoScales`.** Without it, pages appear at their native size.
- **Using UIKit coordinates on a page.** PDF space starts at the bottom-left and
  y grows upward. To place something at a UIKit-style top offset, compute
  `page.bounds(for: .mediaBox).height - topOffset - itemHeight`.
- **Mutating from a background queue.** PDFKit classes are not thread-safe.
  Add or change annotations on the main thread.
- **Reassigning the document in `updateUIView`.** `PDFDocument` is a class;
  compare with `!==` first, otherwise every SwiftUI update reloads the file.

## Review checklist

- [ ] `PDFDocument` is created with optional binding
- [ ] `autoScales = true` is set
- [ ] Every page index is checked against `pageCount`
- [ ] `displayMode` and `displayDirection` match the design
- [ ] Annotation rectangles are in PDF coordinate space
- [ ] All PDFKit mutation happens on the main thread
- [ ] Locked files go through `isLocked` and `unlock(withPassword:)`
- [ ] The SwiftUI wrapper compares documents with `!==` in `updateUIView`
- [ ] `PDFViewPageChanged` drives any page indicator
- [ ] `PDFThumbnailView.pdfView` points at the main `PDFView`
- [ ] Long documents are searched with `beginFindString` and the delegate
- [ ] `write(to:withOptions:)` is used when the file must be encrypted

## References

- [PDFKit patterns](references/pdfkit-patterns.md) - forms, building PDFs, watermarks, merging and splitting, printing, outlines, custom annotation and page classes, overlay lifecycle, rendering, rotation and crop, burn-in, permissions, coordinator wrapper
- [PDFKit](https://developer.apple.com/documentation/pdfkit)
- [PDFView](https://developer.apple.com/documentation/pdfkit/pdfview)
- [PDFDocument](https://developer.apple.com/documentation/pdfkit/pdfdocument)
- [PDFPage](https://developer.apple.com/documentation/pdfkit/pdfpage)
- [PDFAnnotation](https://developer.apple.com/documentation/pdfkit/pdfannotation)
- [PDFSelection](https://developer.apple.com/documentation/pdfkit/pdfselection)
- [PDFThumbnailView](https://developer.apple.com/documentation/pdfkit/pdfthumbnailview)
- [PDFPageOverlayViewProvider](https://developer.apple.com/documentation/pdfkit/pdfpageoverlayviewprovider)
- [Adding widgets to a PDF document](https://developer.apple.com/documentation/pdfkit/adding-widgets-to-a-pdf-document)
- [Adding custom graphics to a PDF](https://developer.apple.com/documentation/pdfkit/adding-custom-graphics-to-a-pdf)
