pdfkit

Display and manipulate PDF documents using PDFKit. Use when embedding PDFView to show PDF files, creating or modifying PDFDocument instances, adding…

INSTALLATION
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill pdfkit
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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

.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 = 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

.highlight

Text markup (yellow highlight)

Underline

.underline

Text markup (underline)

StrikeOut

.strikeOut

Text markup (strikethrough)

Text

.text

Sticky note icon

FreeText

.freeText

Inline text block

Ink

.ink

Freehand drawing paths

Link

.link

URL or page destination

Line

.line

Straight line with endpoints

Square

.square

Rectangle shape

Circle

.circle

Ellipse shape

Stamp

.stamp

Rubber stamp (Approved, etc.)

Widget

.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

  • 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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card